• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.hdmi;
18 
19 import static android.hardware.hdmi.HdmiControlManager.CLEAR_TIMER_STATUS_CEC_DISABLE;
20 import static android.hardware.hdmi.HdmiControlManager.CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION;
21 import static android.hardware.hdmi.HdmiControlManager.CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE;
22 import static android.hardware.hdmi.HdmiControlManager.ONE_TOUCH_RECORD_CEC_DISABLED;
23 import static android.hardware.hdmi.HdmiControlManager.ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION;
24 import static android.hardware.hdmi.HdmiControlManager.ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN;
25 import static android.hardware.hdmi.HdmiControlManager.OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT;
26 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED;
27 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION;
28 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE;
29 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_ANALOGUE;
30 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_DIGITAL;
31 import static android.hardware.hdmi.HdmiControlManager.TIMER_RECORDING_TYPE_EXTERNAL;
32 
33 import android.hardware.hdmi.HdmiControlManager;
34 import android.hardware.hdmi.HdmiDeviceInfo;
35 import android.hardware.hdmi.HdmiRecordSources;
36 import android.hardware.hdmi.HdmiTimerRecordSources;
37 import android.hardware.hdmi.IHdmiControlCallback;
38 import android.media.AudioManager;
39 import android.media.AudioSystem;
40 import android.os.RemoteException;
41 import android.os.SystemProperties;
42 import android.provider.Settings.Global;
43 import android.util.ArraySet;
44 import android.util.Slog;
45 import android.util.SparseArray;
46 
47 import com.android.internal.annotations.GuardedBy;
48 import com.android.internal.util.IndentingPrintWriter;
49 import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
50 import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
51 import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
52 
53 import java.io.UnsupportedEncodingException;
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.Collection;
57 import java.util.Collections;
58 import java.util.Iterator;
59 import java.util.List;
60 
61 /**
62  * Represent a logical device of type TV residing in Android system.
63  */
64 final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
65     private static final String TAG = "HdmiCecLocalDeviceTv";
66 
67     // Whether ARC is available or not. "true" means that ARC is established between TV and
68     // AVR as audio receiver.
69     @ServiceThreadOnly
70     private boolean mArcEstablished = false;
71 
72     // Whether ARC feature is enabled or not. The default value is true.
73     // TODO: once adding system setting for it, read the value to it.
74     private boolean mArcFeatureEnabled = true;
75 
76     // Whether System audio mode is activated or not.
77     // This becomes true only when all system audio sequences are finished.
78     @GuardedBy("mLock")
79     private boolean mSystemAudioActivated = false;
80 
81     // The previous port id (input) before switching to the new one. This is remembered in order to
82     // be able to switch to it upon receiving <Inactive Source> from currently active source.
83     // This remains valid only when the active source was switched via one touch play operation
84     // (either by TV or source device). Manual port switching invalidates this value to
85     // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything.
86     @GuardedBy("mLock")
87     private int mPrevPortId;
88 
89     @GuardedBy("mLock")
90     private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME;
91 
92     @GuardedBy("mLock")
93     private boolean mSystemAudioMute = false;
94 
95     // Copy of mDeviceInfos to guarantee thread-safety.
96     @GuardedBy("mLock")
97     private List<HdmiDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
98     // All external cec input(source) devices. Does not include system audio device.
99     @GuardedBy("mLock")
100     private List<HdmiDeviceInfo> mSafeExternalInputs = Collections.emptyList();
101 
102     // Map-like container of all cec devices including local ones.
103     // device id is used as key of container.
104     // This is not thread-safe. For external purpose use mSafeDeviceInfos.
105     private final SparseArray<HdmiDeviceInfo> mDeviceInfos = new SparseArray<>();
106 
107     // If true, TV going to standby mode puts other devices also to standby.
108     private boolean mAutoDeviceOff;
109 
110     // If true, TV wakes itself up when receiving <Text/Image View On>.
111     private boolean mAutoWakeup;
112 
113     private final HdmiCecStandbyModeHandler mStandbyHandler;
114 
115     // If true, do not do routing control/send active source for internal source.
116     // Set to true when the device was woken up by <Text/Image View On>.
117     private boolean mSkipRoutingControl;
118 
119     // Set of physical addresses of CEC switches on the CEC bus. Managed independently from
120     // other CEC devices since they might not have logical address.
121     private final ArraySet<Integer> mCecSwitches = new ArraySet<Integer>();
122 
HdmiCecLocalDeviceTv(HdmiControlService service)123     HdmiCecLocalDeviceTv(HdmiControlService service) {
124         super(service, HdmiDeviceInfo.DEVICE_TV);
125         mPrevPortId = Constants.INVALID_PORT_ID;
126         mAutoDeviceOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED,
127                 true);
128         mAutoWakeup = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, true);
129         mStandbyHandler = new HdmiCecStandbyModeHandler(service, this);
130     }
131 
132     @Override
133     @ServiceThreadOnly
onAddressAllocated(int logicalAddress, int reason)134     protected void onAddressAllocated(int logicalAddress, int reason) {
135         assertRunOnServiceThread();
136         mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
137                 mAddress, mService.getPhysicalAddress(), mDeviceType));
138         mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
139                 mAddress, mService.getVendorId()));
140         mCecSwitches.add(mService.getPhysicalAddress());  // TV is a CEC switch too.
141         mSkipRoutingControl = (reason == HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE);
142         launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC &&
143                 reason != HdmiControlService.INITIATED_BY_BOOT_UP);
144         launchDeviceDiscovery();
145         startQueuedActions();
146     }
147 
148     @Override
149     @ServiceThreadOnly
getPreferredAddress()150     protected int getPreferredAddress() {
151         assertRunOnServiceThread();
152         return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_TV,
153                 Constants.ADDR_UNREGISTERED);
154     }
155 
156     @Override
157     @ServiceThreadOnly
setPreferredAddress(int addr)158     protected void setPreferredAddress(int addr) {
159         assertRunOnServiceThread();
160         SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_TV, String.valueOf(addr));
161     }
162 
163     @Override
164     @ServiceThreadOnly
dispatchMessage(HdmiCecMessage message)165     boolean dispatchMessage(HdmiCecMessage message) {
166         assertRunOnServiceThread();
167         if (mService.isPowerStandby() && mStandbyHandler.handleCommand(message)) {
168             return true;
169         }
170         return super.onMessage(message);
171     }
172 
173     /**
174      * Performs the action 'device select', or 'one touch play' initiated by TV.
175      *
176      * @param id id of HDMI device to select
177      * @param callback callback object to report the result with
178      */
179     @ServiceThreadOnly
deviceSelect(int id, IHdmiControlCallback callback)180     void deviceSelect(int id, IHdmiControlCallback callback) {
181         assertRunOnServiceThread();
182         HdmiDeviceInfo targetDevice = mDeviceInfos.get(id);
183         if (targetDevice == null) {
184             invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
185             return;
186         }
187         int targetAddress = targetDevice.getLogicalAddress();
188         ActiveSource active = getActiveSource();
189         if (active.isValid() && targetAddress == active.logicalAddress) {
190             invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
191             return;
192         }
193         if (targetAddress == Constants.ADDR_INTERNAL) {
194             handleSelectInternalSource();
195             // Switching to internal source is always successful even when CEC control is disabled.
196             setActiveSource(targetAddress, mService.getPhysicalAddress());
197             setActivePath(mService.getPhysicalAddress());
198             invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
199             return;
200         }
201         if (!mService.isControlEnabled()) {
202             setActiveSource(targetDevice);
203             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
204             return;
205         }
206         removeAction(DeviceSelectAction.class);
207         addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
208     }
209 
210     @ServiceThreadOnly
handleSelectInternalSource()211     private void handleSelectInternalSource() {
212         assertRunOnServiceThread();
213         // Seq #18
214         if (mService.isControlEnabled() && mActiveSource.logicalAddress != mAddress) {
215             updateActiveSource(mAddress, mService.getPhysicalAddress());
216             if (mSkipRoutingControl) {
217                 mSkipRoutingControl = false;
218                 return;
219             }
220             HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(
221                     mAddress, mService.getPhysicalAddress());
222             mService.sendCecCommand(activeSource);
223         }
224     }
225 
226     @ServiceThreadOnly
updateActiveSource(int logicalAddress, int physicalAddress)227     void updateActiveSource(int logicalAddress, int physicalAddress) {
228         assertRunOnServiceThread();
229         updateActiveSource(ActiveSource.of(logicalAddress, physicalAddress));
230     }
231 
232     @ServiceThreadOnly
updateActiveSource(ActiveSource newActive)233     void updateActiveSource(ActiveSource newActive) {
234         assertRunOnServiceThread();
235         // Seq #14
236         if (mActiveSource.equals(newActive)) {
237             return;
238         }
239         setActiveSource(newActive);
240         int logicalAddress = newActive.logicalAddress;
241         if (getCecDeviceInfo(logicalAddress) != null && logicalAddress != mAddress) {
242             if (mService.pathToPortId(newActive.physicalAddress) == getActivePortId()) {
243                 setPrevPortId(getActivePortId());
244             }
245             // TODO: Show the OSD banner related to the new active source device.
246         } else {
247             // TODO: If displayed, remove the OSD banner related to the previous
248             //       active source device.
249         }
250     }
251 
getPortId(int physicalAddress)252     int getPortId(int physicalAddress) {
253         return mService.pathToPortId(physicalAddress);
254     }
255 
256     /**
257      * Returns the previous port id kept to handle input switching on <Inactive Source>.
258      */
getPrevPortId()259     int getPrevPortId() {
260         synchronized (mLock) {
261             return mPrevPortId;
262         }
263     }
264 
265     /**
266      * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be
267      * taken for <Inactive Source>.
268      */
setPrevPortId(int portId)269     void setPrevPortId(int portId) {
270         synchronized (mLock) {
271             mPrevPortId = portId;
272         }
273     }
274 
275     @ServiceThreadOnly
updateActiveInput(int path, boolean notifyInputChange)276     void updateActiveInput(int path, boolean notifyInputChange) {
277         assertRunOnServiceThread();
278         // Seq #15
279         if (path == getActivePath()) {
280             return;
281         }
282         setPrevPortId(getActivePortId());
283         setActivePath(path);
284         // TODO: Handle PAP/PIP case.
285         // Show OSD port change banner
286         if (notifyInputChange) {
287             ActiveSource activeSource = getActiveSource();
288             HdmiDeviceInfo info = getCecDeviceInfo(activeSource.logicalAddress);
289             if (info == null) {
290                 info = new HdmiDeviceInfo(Constants.ADDR_INVALID, path, getActivePortId(),
291                         HdmiDeviceInfo.DEVICE_RESERVED, 0, null);
292             }
293             mService.invokeInputChangeListener(info);
294         }
295     }
296 
297     @ServiceThreadOnly
doManualPortSwitching(int portId, IHdmiControlCallback callback)298     void doManualPortSwitching(int portId, IHdmiControlCallback callback) {
299         assertRunOnServiceThread();
300         // Seq #20
301         if (!mService.isValidPortId(portId)) {
302             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
303             return;
304         }
305         if (portId == getActivePortId()) {
306             invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS);
307             return;
308         }
309         mActiveSource.invalidate();
310         if (!mService.isControlEnabled()) {
311             setActivePortId(portId);
312             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
313             return;
314         }
315         int oldPath = getActivePortId() != Constants.INVALID_PORT_ID
316                 ? mService.portIdToPath(getActivePortId()) : getDeviceInfo().getPhysicalAddress();
317         setActivePath(oldPath);
318         if (mSkipRoutingControl) {
319             mSkipRoutingControl = false;
320             return;
321         }
322         int newPath = mService.portIdToPath(portId);
323         HdmiCecMessage routingChange =
324                 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath);
325         mService.sendCecCommand(routingChange);
326         removeAction(RoutingControlAction.class);
327         addAndStartAction(new RoutingControlAction(this, newPath, true, callback));
328     }
329 
330     @ServiceThreadOnly
getPowerStatus()331     int getPowerStatus() {
332         assertRunOnServiceThread();
333         return mService.getPowerStatus();
334     }
335 
336     /**
337      * Sends key to a target CEC device.
338      *
339      * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}.
340      * @param isPressed true if this is key press event
341      */
342     @Override
343     @ServiceThreadOnly
sendKeyEvent(int keyCode, boolean isPressed)344     protected void sendKeyEvent(int keyCode, boolean isPressed) {
345         assertRunOnServiceThread();
346         if (!HdmiCecKeycode.isSupportedKeycode(keyCode)) {
347             Slog.w(TAG, "Unsupported key: " + keyCode);
348             return;
349         }
350         List<SendKeyAction> action = getActions(SendKeyAction.class);
351         if (!action.isEmpty()) {
352             action.get(0).processKeyEvent(keyCode, isPressed);
353         } else {
354             if (isPressed && getActiveSource().isValid()) {
355                 int logicalAddress = getActiveSource().logicalAddress;
356                 addAndStartAction(new SendKeyAction(this, logicalAddress, keyCode));
357             } else {
358                 Slog.w(TAG, "Discard key event: " + keyCode + " pressed:" + isPressed);
359             }
360         }
361     }
362 
invokeCallback(IHdmiControlCallback callback, int result)363     private static void invokeCallback(IHdmiControlCallback callback, int result) {
364         if (callback == null) {
365             return;
366         }
367         try {
368             callback.onComplete(result);
369         } catch (RemoteException e) {
370             Slog.e(TAG, "Invoking callback failed:" + e);
371         }
372     }
373 
374     @Override
375     @ServiceThreadOnly
handleActiveSource(HdmiCecMessage message)376     protected boolean handleActiveSource(HdmiCecMessage message) {
377         assertRunOnServiceThread();
378         int logicalAddress = message.getSource();
379         int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
380         if (getCecDeviceInfo(logicalAddress) == null) {
381             handleNewDeviceAtTheTailOfActivePath(physicalAddress);
382         } else {
383             ActiveSource activeSource = ActiveSource.of(logicalAddress, physicalAddress);
384             ActiveSourceHandler.create(this, null).process(activeSource);
385         }
386         return true;
387     }
388 
389     @Override
390     @ServiceThreadOnly
handleInactiveSource(HdmiCecMessage message)391     protected boolean handleInactiveSource(HdmiCecMessage message) {
392         assertRunOnServiceThread();
393         // Seq #10
394 
395         // Ignore <Inactive Source> from non-active source device.
396         if (getActiveSource().logicalAddress != message.getSource()) {
397             return true;
398         }
399         if (isProhibitMode()) {
400             return true;
401         }
402         int portId = getPrevPortId();
403         if (portId != Constants.INVALID_PORT_ID) {
404             // TODO: Do this only if TV is not showing multiview like PIP/PAP.
405 
406             HdmiDeviceInfo inactiveSource = getCecDeviceInfo(message.getSource());
407             if (inactiveSource == null) {
408                 return true;
409             }
410             if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) {
411                 return true;
412             }
413             // TODO: Switch the TV freeze mode off
414 
415             doManualPortSwitching(portId, null);
416             setPrevPortId(Constants.INVALID_PORT_ID);
417         }
418         return true;
419     }
420 
421     @Override
422     @ServiceThreadOnly
handleRequestActiveSource(HdmiCecMessage message)423     protected boolean handleRequestActiveSource(HdmiCecMessage message) {
424         assertRunOnServiceThread();
425         // Seq #19
426         if (mAddress == getActiveSource().logicalAddress) {
427             mService.sendCecCommand(
428                     HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath()));
429         }
430         return true;
431     }
432 
433     @Override
434     @ServiceThreadOnly
handleGetMenuLanguage(HdmiCecMessage message)435     protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
436         assertRunOnServiceThread();
437         if (!broadcastMenuLanguage(mService.getLanguage())) {
438             Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
439         }
440         return true;
441     }
442 
443     @ServiceThreadOnly
broadcastMenuLanguage(String language)444     boolean broadcastMenuLanguage(String language) {
445         assertRunOnServiceThread();
446         HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
447                 mAddress, language);
448         if (command != null) {
449             mService.sendCecCommand(command);
450             return true;
451         }
452         return false;
453     }
454 
455     @Override
456     @ServiceThreadOnly
handleReportPhysicalAddress(HdmiCecMessage message)457     protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
458         assertRunOnServiceThread();
459         int path = HdmiUtils.twoBytesToInt(message.getParams());
460         int address = message.getSource();
461         int type = message.getParams()[2];
462 
463         if (updateCecSwitchInfo(address, type, path)) return true;
464 
465         // Ignore if [Device Discovery Action] is going on.
466         if (hasAction(DeviceDiscoveryAction.class)) {
467             Slog.i(TAG, "Ignored while Device Discovery Action is in progress: " + message);
468             return true;
469         }
470 
471         if (!isInDeviceList(address, path)) {
472             handleNewDeviceAtTheTailOfActivePath(path);
473         }
474         startNewDeviceAction(ActiveSource.of(address, path));
475         return true;
476     }
477 
478     @Override
handleReportPowerStatus(HdmiCecMessage command)479     protected boolean handleReportPowerStatus(HdmiCecMessage command) {
480         int newStatus = command.getParams()[0] & 0xFF;
481         updateDevicePowerStatus(command.getSource(), newStatus);
482         return true;
483     }
484 
485     @Override
handleTimerStatus(HdmiCecMessage message)486     protected boolean handleTimerStatus(HdmiCecMessage message) {
487         // Do nothing.
488         return true;
489     }
490 
491     @Override
handleRecordStatus(HdmiCecMessage message)492     protected boolean handleRecordStatus(HdmiCecMessage message) {
493         // Do nothing.
494         return true;
495     }
496 
updateCecSwitchInfo(int address, int type, int path)497     boolean updateCecSwitchInfo(int address, int type, int path) {
498         if (address == Constants.ADDR_UNREGISTERED
499                 && type == HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH) {
500             mCecSwitches.add(path);
501             updateSafeDeviceInfoList();
502             return true;  // Pure switch does not need further processing. Return here.
503         }
504         if (type == HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM) {
505             mCecSwitches.add(path);
506         }
507         return false;
508     }
509 
startNewDeviceAction(ActiveSource activeSource)510     void startNewDeviceAction(ActiveSource activeSource) {
511         for (NewDeviceAction action : getActions(NewDeviceAction.class)) {
512             // If there is new device action which has the same logical address and path
513             // ignore new request.
514             // NewDeviceAction is created whenever it receives <Report Physical Address>.
515             // And there is a chance starting NewDeviceAction for the same source.
516             // Usually, new device sends <Report Physical Address> when it's plugged
517             // in. However, TV can detect a new device from HotPlugDetectionAction,
518             // which sends <Give Physical Address> to the source for newly detected
519             // device.
520             if (action.isActionOf(activeSource)) {
521                 return;
522             }
523         }
524 
525         addAndStartAction(new NewDeviceAction(this, activeSource.logicalAddress,
526                 activeSource.physicalAddress));
527     }
528 
handleNewDeviceAtTheTailOfActivePath(int path)529     private void handleNewDeviceAtTheTailOfActivePath(int path) {
530         // Seq #22
531         if (isTailOfActivePath(path, getActivePath())) {
532             removeAction(RoutingControlAction.class);
533             int newPath = mService.portIdToPath(getActivePortId());
534             setActivePath(newPath);
535             mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
536                     mAddress, getActivePath(), newPath));
537             addAndStartAction(new RoutingControlAction(this, newPath, false, null));
538         }
539     }
540 
541     /**
542      * Whether the given path is located in the tail of current active path.
543      *
544      * @param path to be tested
545      * @param activePath current active path
546      * @return true if the given path is located in the tail of current active path; otherwise,
547      *         false
548      */
isTailOfActivePath(int path, int activePath)549     static boolean isTailOfActivePath(int path, int activePath) {
550         // If active routing path is internal source, return false.
551         if (activePath == 0) {
552             return false;
553         }
554         for (int i = 12; i >= 0; i -= 4) {
555             int curActivePath = (activePath >> i) & 0xF;
556             if (curActivePath == 0) {
557                 return true;
558             } else {
559                 int curPath = (path >> i) & 0xF;
560                 if (curPath != curActivePath) {
561                     return false;
562                 }
563             }
564         }
565         return false;
566     }
567 
568     @Override
569     @ServiceThreadOnly
handleRoutingChange(HdmiCecMessage message)570     protected boolean handleRoutingChange(HdmiCecMessage message) {
571         assertRunOnServiceThread();
572         // Seq #21
573         byte[] params = message.getParams();
574         int currentPath = HdmiUtils.twoBytesToInt(params);
575         if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) {
576             mActiveSource.invalidate();
577             removeAction(RoutingControlAction.class);
578             int newPath = HdmiUtils.twoBytesToInt(params, 2);
579             addAndStartAction(new RoutingControlAction(this, newPath, true, null));
580         }
581         return true;
582     }
583 
584     @Override
585     @ServiceThreadOnly
handleReportAudioStatus(HdmiCecMessage message)586     protected boolean handleReportAudioStatus(HdmiCecMessage message) {
587         assertRunOnServiceThread();
588 
589         byte params[] = message.getParams();
590         int mute = params[0] & 0x80;
591         int volume = params[0] & 0x7F;
592         setAudioStatus(mute == 0x80, volume);
593         return true;
594     }
595 
596     @Override
597     @ServiceThreadOnly
handleTextViewOn(HdmiCecMessage message)598     protected boolean handleTextViewOn(HdmiCecMessage message) {
599         assertRunOnServiceThread();
600         if (mService.isPowerStandbyOrTransient() && mAutoWakeup) {
601             mService.wakeUp();
602         }
603         return true;
604     }
605 
606     @Override
607     @ServiceThreadOnly
handleImageViewOn(HdmiCecMessage message)608     protected boolean handleImageViewOn(HdmiCecMessage message) {
609         assertRunOnServiceThread();
610         // Currently, it's the same as <Text View On>.
611         return handleTextViewOn(message);
612     }
613 
614     @Override
615     @ServiceThreadOnly
handleSetOsdName(HdmiCecMessage message)616     protected boolean handleSetOsdName(HdmiCecMessage message) {
617         int source = message.getSource();
618         HdmiDeviceInfo deviceInfo = getCecDeviceInfo(source);
619         // If the device is not in device list, ignore it.
620         if (deviceInfo == null) {
621             Slog.e(TAG, "No source device info for <Set Osd Name>." + message);
622             return true;
623         }
624         String osdName = null;
625         try {
626             osdName = new String(message.getParams(), "US-ASCII");
627         } catch (UnsupportedEncodingException e) {
628             Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e);
629             return true;
630         }
631 
632         if (deviceInfo.getDisplayName().equals(osdName)) {
633             Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message);
634             return true;
635         }
636 
637         addCecDevice(new HdmiDeviceInfo(deviceInfo.getLogicalAddress(),
638                 deviceInfo.getPhysicalAddress(), deviceInfo.getPortId(),
639                 deviceInfo.getDeviceType(), deviceInfo.getVendorId(), osdName));
640         return true;
641     }
642 
643     @ServiceThreadOnly
launchDeviceDiscovery()644     private void launchDeviceDiscovery() {
645         assertRunOnServiceThread();
646         clearDeviceInfoList();
647         DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
648                 new DeviceDiscoveryCallback() {
649                     @Override
650                     public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
651                         for (HdmiDeviceInfo info : deviceInfos) {
652                             addCecDevice(info);
653                         }
654 
655                         // Since we removed all devices when it's start and
656                         // device discovery action does not poll local devices,
657                         // we should put device info of local device manually here
658                         for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
659                             addCecDevice(device.getDeviceInfo());
660                         }
661 
662                         addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
663                         addAndStartAction(new PowerStatusMonitorAction(HdmiCecLocalDeviceTv.this));
664 
665                         // If there is AVR, initiate System Audio Auto initiation action,
666                         // which turns on and off system audio according to last system
667                         // audio setting.
668                         HdmiDeviceInfo avr = getAvrDeviceInfo();
669                         if (avr != null) {
670                             onNewAvrAdded(avr);
671                         }
672                     }
673                 });
674         addAndStartAction(action);
675     }
676 
677     @ServiceThreadOnly
onNewAvrAdded(HdmiDeviceInfo avr)678     void onNewAvrAdded(HdmiDeviceInfo avr) {
679         assertRunOnServiceThread();
680         if (getSystemAudioModeSetting()) {
681             addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress()));
682         }
683         if (isArcFeatureEnabled()) {
684             startArcAction(true);
685         }
686     }
687 
688     // Clear all device info.
689     @ServiceThreadOnly
clearDeviceInfoList()690     private void clearDeviceInfoList() {
691         assertRunOnServiceThread();
692         for (HdmiDeviceInfo info : mSafeExternalInputs) {
693             invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
694         }
695         mDeviceInfos.clear();
696         updateSafeDeviceInfoList();
697     }
698 
699     @ServiceThreadOnly
700     // Seq #32
changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback)701     void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
702         assertRunOnServiceThread();
703         if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
704             setSystemAudioMode(false, true);
705             invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
706             return;
707         }
708         HdmiDeviceInfo avr = getAvrDeviceInfo();
709         if (avr == null) {
710             setSystemAudioMode(false, true);
711             invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
712             return;
713         }
714 
715         addAndStartAction(
716                 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
717     }
718 
719     // # Seq 25
setSystemAudioMode(boolean on, boolean updateSetting)720     void setSystemAudioMode(boolean on, boolean updateSetting) {
721         HdmiLogger.debug("System Audio Mode change[old:%b new:%b]", mSystemAudioActivated, on);
722 
723         if (updateSetting) {
724             mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
725         }
726         updateAudioManagerForSystemAudio(on);
727         synchronized (mLock) {
728             if (mSystemAudioActivated != on) {
729                 mSystemAudioActivated = on;
730                 mService.announceSystemAudioModeChange(on);
731             }
732         }
733     }
734 
updateAudioManagerForSystemAudio(boolean on)735     private void updateAudioManagerForSystemAudio(boolean on) {
736         int device = mService.getAudioManager().setHdmiSystemAudioSupported(on);
737         HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device);
738     }
739 
isSystemAudioActivated()740     boolean isSystemAudioActivated() {
741         if (getAvrDeviceInfo() == null) {
742             return false;
743         }
744         synchronized (mLock) {
745             return mSystemAudioActivated;
746         }
747     }
748 
getSystemAudioModeSetting()749     boolean getSystemAudioModeSetting() {
750         return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false);
751     }
752 
753     /**
754      * Change ARC status into the given {@code enabled} status.
755      *
756      * @return {@code true} if ARC was in "Enabled" status
757      */
758     @ServiceThreadOnly
setArcStatus(boolean enabled)759     boolean setArcStatus(boolean enabled) {
760         assertRunOnServiceThread();
761 
762         HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled);
763         boolean oldStatus = mArcEstablished;
764         // 1. Enable/disable ARC circuit.
765         mService.setAudioReturnChannel(enabled);
766         // 2. Notify arc status to audio service.
767         notifyArcStatusToAudioService(enabled);
768         // 3. Update arc status;
769         mArcEstablished = enabled;
770         return oldStatus;
771     }
772 
notifyArcStatusToAudioService(boolean enabled)773     private void notifyArcStatusToAudioService(boolean enabled) {
774         // Note that we don't set any name to ARC.
775         mService.getAudioManager().setWiredDeviceConnectionState(
776                 AudioSystem.DEVICE_OUT_HDMI_ARC,
777                 enabled ? 1 : 0, "");
778     }
779 
780     /**
781      * Returns whether ARC is enabled or not.
782      */
783     @ServiceThreadOnly
isArcEstabilished()784     boolean isArcEstabilished() {
785         assertRunOnServiceThread();
786         return mArcFeatureEnabled && mArcEstablished;
787     }
788 
789     @ServiceThreadOnly
changeArcFeatureEnabled(boolean enabled)790     void changeArcFeatureEnabled(boolean enabled) {
791         assertRunOnServiceThread();
792 
793         if (mArcFeatureEnabled != enabled) {
794             mArcFeatureEnabled = enabled;
795             if (enabled) {
796                 if (!mArcEstablished) {
797                     startArcAction(true);
798                 }
799             } else {
800                 if (mArcEstablished) {
801                     startArcAction(false);
802                 }
803             }
804         }
805     }
806 
807     @ServiceThreadOnly
isArcFeatureEnabled()808     boolean isArcFeatureEnabled() {
809         assertRunOnServiceThread();
810         return mArcFeatureEnabled;
811     }
812 
813     @ServiceThreadOnly
startArcAction(boolean enabled)814     void startArcAction(boolean enabled) {
815         assertRunOnServiceThread();
816         HdmiDeviceInfo info = getAvrDeviceInfo();
817         if (info == null) {
818             Slog.w(TAG, "Failed to start arc action; No AVR device.");
819             return;
820         }
821         if (!canStartArcUpdateAction(info.getLogicalAddress(), enabled)) {
822             Slog.w(TAG, "Failed to start arc action; ARC configuration check failed.");
823             if (enabled && !isConnectedToArcPort(info.getPhysicalAddress())) {
824                 displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
825             }
826             return;
827         }
828 
829         // Terminate opposite action and start action if not exist.
830         if (enabled) {
831             removeAction(RequestArcTerminationAction.class);
832             if (!hasAction(RequestArcInitiationAction.class)) {
833                 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
834             }
835         } else {
836             removeAction(RequestArcInitiationAction.class);
837             if (!hasAction(RequestArcTerminationAction.class)) {
838                 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
839             }
840         }
841     }
842 
isDirectConnectAddress(int physicalAddress)843     private boolean isDirectConnectAddress(int physicalAddress) {
844         return (physicalAddress & Constants.ROUTING_PATH_TOP_MASK) == physicalAddress;
845     }
846 
setAudioStatus(boolean mute, int volume)847     void setAudioStatus(boolean mute, int volume) {
848         synchronized (mLock) {
849             mSystemAudioMute = mute;
850             mSystemAudioVolume = volume;
851             int maxVolume = mService.getAudioManager().getStreamMaxVolume(
852                     AudioManager.STREAM_MUSIC);
853             mService.setAudioStatus(mute,
854                     VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
855             displayOsd(HdmiControlManager.OSD_MESSAGE_AVR_VOLUME_CHANGED,
856                     mute ? HdmiControlManager.AVR_VOLUME_MUTED : volume);
857         }
858     }
859 
860     @ServiceThreadOnly
changeVolume(int curVolume, int delta, int maxVolume)861     void changeVolume(int curVolume, int delta, int maxVolume) {
862         assertRunOnServiceThread();
863         if (delta == 0 || !isSystemAudioActivated()) {
864             return;
865         }
866 
867         int targetVolume = curVolume + delta;
868         int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
869         synchronized (mLock) {
870             // If new volume is the same as current system audio volume, just ignore it.
871             // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
872             if (cecVolume == mSystemAudioVolume) {
873                 // Update tv volume with system volume value.
874                 mService.setAudioStatus(false,
875                         VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
876                 return;
877             }
878         }
879 
880         List<VolumeControlAction> actions = getActions(VolumeControlAction.class);
881         if (actions.isEmpty()) {
882             addAndStartAction(new VolumeControlAction(this,
883                     getAvrDeviceInfo().getLogicalAddress(), delta > 0));
884         } else {
885             actions.get(0).handleVolumeChange(delta > 0);
886         }
887     }
888 
889     @ServiceThreadOnly
changeMute(boolean mute)890     void changeMute(boolean mute) {
891         assertRunOnServiceThread();
892         HdmiLogger.debug("[A]:Change mute:%b", mute);
893         synchronized (mLock) {
894             if (mSystemAudioMute == mute) {
895                 HdmiLogger.debug("No need to change mute.");
896                 return;
897             }
898         }
899         if (!isSystemAudioActivated()) {
900             HdmiLogger.debug("[A]:System audio is not activated.");
901             return;
902         }
903 
904         // Remove existing volume action.
905         removeAction(VolumeControlAction.class);
906         sendUserControlPressedAndReleased(getAvrDeviceInfo().getLogicalAddress(),
907                 mute ? HdmiCecKeycode.CEC_KEYCODE_MUTE_FUNCTION :
908                         HdmiCecKeycode.CEC_KEYCODE_RESTORE_VOLUME_FUNCTION);
909     }
910 
911     @Override
912     @ServiceThreadOnly
handleInitiateArc(HdmiCecMessage message)913     protected boolean handleInitiateArc(HdmiCecMessage message) {
914         assertRunOnServiceThread();
915 
916         if (!canStartArcUpdateAction(message.getSource(), true)) {
917             mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
918             if (!isConnectedToArcPort(message.getSource())) {
919                 displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
920             }
921             return true;
922         }
923 
924         // In case where <Initiate Arc> is started by <Request ARC Initiation>
925         // need to clean up RequestArcInitiationAction.
926         removeAction(RequestArcInitiationAction.class);
927         SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
928                 message.getSource(), true);
929         addAndStartAction(action);
930         return true;
931     }
932 
canStartArcUpdateAction(int avrAddress, boolean shouldCheckArcFeatureEnabled)933     private boolean canStartArcUpdateAction(int avrAddress, boolean shouldCheckArcFeatureEnabled) {
934         HdmiDeviceInfo avr = getAvrDeviceInfo();
935         if (avr != null
936                 && (avrAddress == avr.getLogicalAddress())
937                 && isConnectedToArcPort(avr.getPhysicalAddress())
938                 && isDirectConnectAddress(avr.getPhysicalAddress())) {
939             if (shouldCheckArcFeatureEnabled) {
940                 return isArcFeatureEnabled();
941             } else {
942                 return true;
943             }
944         } else {
945             return false;
946         }
947     }
948 
949     @Override
950     @ServiceThreadOnly
handleTerminateArc(HdmiCecMessage message)951     protected boolean handleTerminateArc(HdmiCecMessage message) {
952         assertRunOnServiceThread();
953         // In cast of termination, do not check ARC configuration in that AVR device
954         // might be removed already.
955 
956         // In case where <Terminate Arc> is started by <Request ARC Termination>
957         // need to clean up RequestArcInitiationAction.
958         removeAction(RequestArcTerminationAction.class);
959         SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
960                 message.getSource(), false);
961         addAndStartAction(action);
962         return true;
963     }
964 
965     @Override
966     @ServiceThreadOnly
handleSetSystemAudioMode(HdmiCecMessage message)967     protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
968         assertRunOnServiceThread();
969         if (!isMessageForSystemAudio(message)) {
970             HdmiLogger.warning("Invalid <Set System Audio Mode> message:" + message);
971             mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
972             return true;
973         }
974         SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
975                 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
976         addAndStartAction(action);
977         return true;
978     }
979 
980     @Override
981     @ServiceThreadOnly
handleSystemAudioModeStatus(HdmiCecMessage message)982     protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
983         assertRunOnServiceThread();
984         if (!isMessageForSystemAudio(message)) {
985             HdmiLogger.warning("Invalid <System Audio Mode Status> message:" + message);
986             // Ignore this message.
987             return true;
988         }
989         setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
990         return true;
991     }
992 
993     // Seq #53
994     @Override
995     @ServiceThreadOnly
handleRecordTvScreen(HdmiCecMessage message)996     protected boolean handleRecordTvScreen(HdmiCecMessage message) {
997         List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
998         if (!actions.isEmpty()) {
999             // Assumes only one OneTouchRecordAction.
1000             OneTouchRecordAction action = actions.get(0);
1001             if (action.getRecorderAddress() != message.getSource()) {
1002                 announceOneTouchRecordResult(
1003                         HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS);
1004             }
1005             return super.handleRecordTvScreen(message);
1006         }
1007 
1008         int recorderAddress = message.getSource();
1009         byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress);
1010         int reason = startOneTouchRecord(recorderAddress, recordSource);
1011         if (reason != Constants.ABORT_NO_ERROR) {
1012             mService.maySendFeatureAbortCommand(message, reason);
1013         }
1014         return true;
1015     }
1016 
1017     @Override
handleTimerClearedStatus(HdmiCecMessage message)1018     protected boolean handleTimerClearedStatus(HdmiCecMessage message) {
1019         byte[] params = message.getParams();
1020         int timerClearedStatusData = params[0] & 0xFF;
1021         announceTimerRecordingResult(timerClearedStatusData);
1022         return true;
1023     }
1024 
announceOneTouchRecordResult(int result)1025     void announceOneTouchRecordResult(int result) {
1026         mService.invokeOneTouchRecordResult(result);
1027     }
1028 
announceTimerRecordingResult(int result)1029     void announceTimerRecordingResult(int result) {
1030         mService.invokeTimerRecordingResult(result);
1031     }
1032 
announceClearTimerRecordingResult(int result)1033     void announceClearTimerRecordingResult(int result) {
1034         mService.invokeClearTimerRecordingResult(result);
1035     }
1036 
isMessageForSystemAudio(HdmiCecMessage message)1037     private boolean isMessageForSystemAudio(HdmiCecMessage message) {
1038         return mService.isControlEnabled()
1039                 && message.getSource() == Constants.ADDR_AUDIO_SYSTEM
1040                 && (message.getDestination() == Constants.ADDR_TV
1041                         || message.getDestination() == Constants.ADDR_BROADCAST)
1042                 && getAvrDeviceInfo() != null;
1043     }
1044 
1045     /**
1046      * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
1047      * logical address as new device info's.
1048      *
1049      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1050      *
1051      * @param deviceInfo a new {@link HdmiDeviceInfo} to be added.
1052      * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo}
1053      *         that has the same logical address as new one has.
1054      */
1055     @ServiceThreadOnly
addDeviceInfo(HdmiDeviceInfo deviceInfo)1056     private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
1057         assertRunOnServiceThread();
1058         HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress());
1059         if (oldDeviceInfo != null) {
1060             removeDeviceInfo(deviceInfo.getId());
1061         }
1062         mDeviceInfos.append(deviceInfo.getId(), deviceInfo);
1063         updateSafeDeviceInfoList();
1064         return oldDeviceInfo;
1065     }
1066 
1067     /**
1068      * Remove a device info corresponding to the given {@code logicalAddress}.
1069      * It returns removed {@link HdmiDeviceInfo} if exists.
1070      *
1071      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1072      *
1073      * @param id id of device to be removed
1074      * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null}
1075      */
1076     @ServiceThreadOnly
removeDeviceInfo(int id)1077     private HdmiDeviceInfo removeDeviceInfo(int id) {
1078         assertRunOnServiceThread();
1079         HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id);
1080         if (deviceInfo != null) {
1081             mDeviceInfos.remove(id);
1082         }
1083         updateSafeDeviceInfoList();
1084         return deviceInfo;
1085     }
1086 
1087     /**
1088      * Return a list of all {@link HdmiDeviceInfo}.
1089      *
1090      * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1091      * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which
1092      * does not include local device.
1093      */
1094     @ServiceThreadOnly
getDeviceInfoList(boolean includeLocalDevice)1095     List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
1096         assertRunOnServiceThread();
1097         if (includeLocalDevice) {
1098             return HdmiUtils.sparseArrayToList(mDeviceInfos);
1099         } else {
1100             ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1101             for (int i = 0; i < mDeviceInfos.size(); ++i) {
1102                 HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1103                 if (!isLocalDeviceAddress(info.getLogicalAddress())) {
1104                     infoList.add(info);
1105                 }
1106             }
1107             return infoList;
1108         }
1109     }
1110 
1111     /**
1112      * Return external input devices.
1113      */
getSafeExternalInputsLocked()1114     List<HdmiDeviceInfo> getSafeExternalInputsLocked() {
1115         return mSafeExternalInputs;
1116     }
1117 
1118     @ServiceThreadOnly
updateSafeDeviceInfoList()1119     private void updateSafeDeviceInfoList() {
1120         assertRunOnServiceThread();
1121         List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
1122         List<HdmiDeviceInfo> externalInputs = getInputDevices();
1123         synchronized (mLock) {
1124             mSafeAllDeviceInfos = copiedDevices;
1125             mSafeExternalInputs = externalInputs;
1126         }
1127     }
1128 
1129     /**
1130      * Return a list of external cec input (source) devices.
1131      *
1132      * <p>Note that this effectively excludes non-source devices like system audio,
1133      * secondary TV.
1134      */
getInputDevices()1135     private List<HdmiDeviceInfo> getInputDevices() {
1136         ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1137         for (int i = 0; i < mDeviceInfos.size(); ++i) {
1138             HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1139             if (isLocalDeviceAddress(info.getLogicalAddress())) {
1140                 continue;
1141             }
1142             if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1143                 infoList.add(info);
1144             }
1145         }
1146         return infoList;
1147     }
1148 
1149     // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch.
1150     // Returns true if the policy is set to true, and the device to check does not have
1151     // a parent CEC device (which should be the CEC-enabled switch) in the list.
hideDevicesBehindLegacySwitch(HdmiDeviceInfo info)1152     private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) {
1153         return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH
1154                 && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches);
1155     }
1156 
isConnectedToCecSwitch(int path, Collection<Integer> switches)1157     private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) {
1158         for (int switchPath : switches) {
1159             if (isParentPath(switchPath, path)) {
1160                 return true;
1161             }
1162         }
1163         return false;
1164     }
1165 
isParentPath(int parentPath, int childPath)1166     private static boolean isParentPath(int parentPath, int childPath) {
1167         // (A000, AB00) (AB00, ABC0), (ABC0, ABCD)
1168         // If child's last non-zero nibble is removed, the result equals to the parent.
1169         for (int i = 0; i <= 12; i += 4) {
1170             int nibble = (childPath >> i) & 0xF;
1171             if (nibble != 0) {
1172                 int parentNibble = (parentPath >> i) & 0xF;
1173                 return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4);
1174             }
1175         }
1176         return false;
1177     }
1178 
invokeDeviceEventListener(HdmiDeviceInfo info, int status)1179     private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {
1180         if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1181             mService.invokeDeviceEventListeners(info, status);
1182         }
1183     }
1184 
1185     @ServiceThreadOnly
isLocalDeviceAddress(int address)1186     private boolean isLocalDeviceAddress(int address) {
1187         assertRunOnServiceThread();
1188         for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
1189             if (device.isAddressOf(address)) {
1190                 return true;
1191             }
1192         }
1193         return false;
1194     }
1195 
1196     @ServiceThreadOnly
getAvrDeviceInfo()1197     HdmiDeviceInfo getAvrDeviceInfo() {
1198         assertRunOnServiceThread();
1199         return getCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1200     }
1201 
1202     /**
1203      * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}.
1204      *
1205      * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}.
1206      *
1207      * @param logicalAddress logical address of the device to be retrieved
1208      * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1209      *         Returns null if no logical address matched
1210      */
1211     @ServiceThreadOnly
getCecDeviceInfo(int logicalAddress)1212     HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) {
1213         assertRunOnServiceThread();
1214         return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
1215     }
1216 
hasSystemAudioDevice()1217     boolean hasSystemAudioDevice() {
1218         return getSafeAvrDeviceInfo() != null;
1219     }
1220 
getSafeAvrDeviceInfo()1221     HdmiDeviceInfo getSafeAvrDeviceInfo() {
1222         return getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1223     }
1224 
1225     /**
1226      * Thread safe version of {@link #getCecDeviceInfo(int)}.
1227      *
1228      * @param logicalAddress logical address to be retrieved
1229      * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1230      *         Returns null if no logical address matched
1231      */
getSafeCecDeviceInfo(int logicalAddress)1232     HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) {
1233         synchronized (mLock) {
1234             for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1235                 if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) {
1236                     return info;
1237                 }
1238             }
1239             return null;
1240         }
1241     }
1242 
1243     /**
1244      * Called when a device is newly added or a new device is detected or
1245      * existing device is updated.
1246      *
1247      * @param info device info of a new device.
1248      */
1249     @ServiceThreadOnly
addCecDevice(HdmiDeviceInfo info)1250     final void addCecDevice(HdmiDeviceInfo info) {
1251         assertRunOnServiceThread();
1252         addDeviceInfo(info);
1253         if (info.getLogicalAddress() == mAddress) {
1254             // The addition of TV device itself should not be notified.
1255             return;
1256         }
1257         invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
1258     }
1259 
1260     /**
1261      * Called when a device is removed or removal of device is detected.
1262      *
1263      * @param address a logical address of a device to be removed
1264      */
1265     @ServiceThreadOnly
removeCecDevice(int address)1266     final void removeCecDevice(int address) {
1267         assertRunOnServiceThread();
1268         HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
1269 
1270         mCecMessageCache.flushMessagesFrom(address);
1271         invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
1272     }
1273 
1274     @ServiceThreadOnly
handleRemoveActiveRoutingPath(int path)1275     void handleRemoveActiveRoutingPath(int path) {
1276         assertRunOnServiceThread();
1277         // Seq #23
1278         if (isTailOfActivePath(path, getActivePath())) {
1279             removeAction(RoutingControlAction.class);
1280             int newPath = mService.portIdToPath(getActivePortId());
1281             mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
1282                     mAddress, getActivePath(), newPath));
1283             mActiveSource.invalidate();
1284             addAndStartAction(new RoutingControlAction(this, getActivePath(), true, null));
1285         }
1286     }
1287 
1288     /**
1289      * Launch routing control process.
1290      *
1291      * @param routingForBootup true if routing control is initiated due to One Touch Play
1292      *        or TV power on
1293      */
1294     @ServiceThreadOnly
launchRoutingControl(boolean routingForBootup)1295     void launchRoutingControl(boolean routingForBootup) {
1296         assertRunOnServiceThread();
1297         // Seq #24
1298         if (getActivePortId() != Constants.INVALID_PORT_ID) {
1299             if (!routingForBootup && !isProhibitMode()) {
1300                 removeAction(RoutingControlAction.class);
1301                 int newPath = mService.portIdToPath(getActivePortId());
1302                 setActivePath(newPath);
1303                 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
1304                         getActivePath(), newPath));
1305                 addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1306                         routingForBootup, null));
1307             }
1308         } else {
1309             int activePath = mService.getPhysicalAddress();
1310             setActivePath(activePath);
1311             if (!routingForBootup) {
1312                 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1313                         activePath));
1314             }
1315         }
1316     }
1317 
1318     /**
1319      * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
1320      * the given routing path. CEC devices use routing path for its physical address to
1321      * describe the hierarchy of the devices in the network.
1322      *
1323      * @param path routing path or physical address
1324      * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
1325      */
1326     @ServiceThreadOnly
getDeviceInfoByPath(int path)1327     final HdmiDeviceInfo getDeviceInfoByPath(int path) {
1328         assertRunOnServiceThread();
1329         for (HdmiDeviceInfo info : getDeviceInfoList(false)) {
1330             if (info.getPhysicalAddress() == path) {
1331                 return info;
1332             }
1333         }
1334         return null;
1335     }
1336 
1337     /**
1338      * Whether a device of the specified physical address and logical address exists
1339      * in a device info list. However, both are minimal condition and it could
1340      * be different device from the original one.
1341      *
1342      * @param logicalAddress logical address of a device to be searched
1343      * @param physicalAddress physical address of a device to be searched
1344      * @return true if exist; otherwise false
1345      */
1346     @ServiceThreadOnly
isInDeviceList(int logicalAddress, int physicalAddress)1347     private boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1348         assertRunOnServiceThread();
1349         HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress);
1350         if (device == null) {
1351             return false;
1352         }
1353         return device.getPhysicalAddress() == physicalAddress;
1354     }
1355 
1356     @Override
1357     @ServiceThreadOnly
onHotplug(int portId, boolean connected)1358     void onHotplug(int portId, boolean connected) {
1359         assertRunOnServiceThread();
1360 
1361         if (!connected) {
1362             removeCecSwitches(portId);
1363         }
1364         // Tv device will have permanent HotplugDetectionAction.
1365         List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1366         if (!hotplugActions.isEmpty()) {
1367             // Note that hotplug action is single action running on a machine.
1368             // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1369             // It covers seq #40, #43.
1370             hotplugActions.get(0).pollAllDevicesNow();
1371         }
1372     }
1373 
removeCecSwitches(int portId)1374     private void removeCecSwitches(int portId) {
1375         Iterator<Integer> it = mCecSwitches.iterator();
1376         while (!it.hasNext()) {
1377             int path = it.next();
1378             if (pathToPortId(path) == portId) {
1379                 it.remove();
1380             }
1381         }
1382     }
1383 
1384     @ServiceThreadOnly
setAutoDeviceOff(boolean enabled)1385     void setAutoDeviceOff(boolean enabled) {
1386         assertRunOnServiceThread();
1387         mAutoDeviceOff = enabled;
1388     }
1389 
1390     @ServiceThreadOnly
setAutoWakeup(boolean enabled)1391     void setAutoWakeup(boolean enabled) {
1392         assertRunOnServiceThread();
1393         mAutoWakeup = enabled;
1394     }
1395 
1396     @ServiceThreadOnly
getAutoWakeup()1397     boolean getAutoWakeup() {
1398         assertRunOnServiceThread();
1399         return mAutoWakeup;
1400     }
1401 
1402     @Override
1403     @ServiceThreadOnly
disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback)1404     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1405         super.disableDevice(initiatedByCec, callback);
1406         assertRunOnServiceThread();
1407         // Remove any repeated working actions.
1408         // HotplugDetectionAction will be reinstated during the wake up process.
1409         // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1410         //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1411         removeAction(DeviceDiscoveryAction.class);
1412         removeAction(HotplugDetectionAction.class);
1413         removeAction(PowerStatusMonitorAction.class);
1414         // Remove recording actions.
1415         removeAction(OneTouchRecordAction.class);
1416         removeAction(TimerRecordingAction.class);
1417 
1418         disableSystemAudioIfExist();
1419         disableArcIfExist();
1420         clearDeviceInfoList();
1421         checkIfPendingActionsCleared();
1422     }
1423 
1424     @ServiceThreadOnly
disableSystemAudioIfExist()1425     private void disableSystemAudioIfExist() {
1426         assertRunOnServiceThread();
1427         if (getAvrDeviceInfo() == null) {
1428             return;
1429         }
1430 
1431         // Seq #31.
1432         removeAction(SystemAudioActionFromAvr.class);
1433         removeAction(SystemAudioActionFromTv.class);
1434         removeAction(SystemAudioAutoInitiationAction.class);
1435         removeAction(SystemAudioStatusAction.class);
1436         removeAction(VolumeControlAction.class);
1437 
1438         // Turn off the mode but do not write it the settings, so that the next time TV powers on
1439         // the system audio mode setting can be restored automatically.
1440         setSystemAudioMode(false, false);
1441     }
1442 
1443     @ServiceThreadOnly
disableArcIfExist()1444     private void disableArcIfExist() {
1445         assertRunOnServiceThread();
1446         HdmiDeviceInfo avr = getAvrDeviceInfo();
1447         if (avr == null) {
1448             return;
1449         }
1450 
1451         // Seq #44.
1452         removeAction(RequestArcInitiationAction.class);
1453         if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1454             addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1455         }
1456     }
1457 
1458     @Override
1459     @ServiceThreadOnly
onStandby(boolean initiatedByCec)1460     protected void onStandby(boolean initiatedByCec) {
1461         assertRunOnServiceThread();
1462         // Seq #11
1463         if (!mService.isControlEnabled()) {
1464             return;
1465         }
1466         if (!initiatedByCec && mAutoDeviceOff) {
1467             mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1468                     mAddress, Constants.ADDR_BROADCAST));
1469         }
1470     }
1471 
isProhibitMode()1472     boolean isProhibitMode() {
1473         return mService.isProhibitMode();
1474     }
1475 
isPowerStandbyOrTransient()1476     boolean isPowerStandbyOrTransient() {
1477         return mService.isPowerStandbyOrTransient();
1478     }
1479 
1480     @ServiceThreadOnly
displayOsd(int messageId)1481     void displayOsd(int messageId) {
1482         assertRunOnServiceThread();
1483         mService.displayOsd(messageId);
1484     }
1485 
1486     @ServiceThreadOnly
displayOsd(int messageId, int extra)1487     void displayOsd(int messageId, int extra) {
1488         assertRunOnServiceThread();
1489         mService.displayOsd(messageId, extra);
1490     }
1491 
1492     // Seq #54 and #55
1493     @ServiceThreadOnly
startOneTouchRecord(int recorderAddress, byte[] recordSource)1494     int startOneTouchRecord(int recorderAddress, byte[] recordSource) {
1495         assertRunOnServiceThread();
1496         if (!mService.isControlEnabled()) {
1497             Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1498             announceOneTouchRecordResult(ONE_TOUCH_RECORD_CEC_DISABLED);
1499             return Constants.ABORT_NOT_IN_CORRECT_MODE;
1500         }
1501 
1502         if (!checkRecorder(recorderAddress)) {
1503             Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1504             announceOneTouchRecordResult(ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1505             return Constants.ABORT_NOT_IN_CORRECT_MODE;
1506         }
1507 
1508         if (!checkRecordSource(recordSource)) {
1509             Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1510             announceOneTouchRecordResult(ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN);
1511             return Constants.ABORT_UNABLE_TO_DETERMINE;
1512         }
1513 
1514         addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource));
1515         Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:"
1516                 + Arrays.toString(recordSource));
1517         return Constants.ABORT_NO_ERROR;
1518     }
1519 
1520     @ServiceThreadOnly
stopOneTouchRecord(int recorderAddress)1521     void stopOneTouchRecord(int recorderAddress) {
1522         assertRunOnServiceThread();
1523         if (!mService.isControlEnabled()) {
1524             Slog.w(TAG, "Can not stop one touch record. CEC control is disabled.");
1525             announceOneTouchRecordResult(ONE_TOUCH_RECORD_CEC_DISABLED);
1526             return;
1527         }
1528 
1529         if (!checkRecorder(recorderAddress)) {
1530             Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1531             announceOneTouchRecordResult(ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1532             return;
1533         }
1534 
1535         // Remove one touch record action so that other one touch record can be started.
1536         removeAction(OneTouchRecordAction.class);
1537         mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress));
1538         Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress);
1539     }
1540 
checkRecorder(int recorderAddress)1541     private boolean checkRecorder(int recorderAddress) {
1542         HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress);
1543         return (device != null)
1544                 && (HdmiUtils.getTypeFromAddress(recorderAddress)
1545                         == HdmiDeviceInfo.DEVICE_RECORDER);
1546     }
1547 
checkRecordSource(byte[] recordSource)1548     private boolean checkRecordSource(byte[] recordSource) {
1549         return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource);
1550     }
1551 
1552     @ServiceThreadOnly
startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource)1553     void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1554         assertRunOnServiceThread();
1555         if (!mService.isControlEnabled()) {
1556             Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1557             announceTimerRecordingResult(TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED);
1558             return;
1559         }
1560 
1561         if (!checkRecorder(recorderAddress)) {
1562             Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1563             announceTimerRecordingResult(
1564                     TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION);
1565             return;
1566         }
1567 
1568         if (!checkTimerRecordingSource(sourceType, recordSource)) {
1569             Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1570             announceTimerRecordingResult(
1571                     TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE);
1572             return;
1573         }
1574 
1575         addAndStartAction(
1576                 new TimerRecordingAction(this, recorderAddress, sourceType, recordSource));
1577         Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:"
1578                 + sourceType + ", RecordSource:" + Arrays.toString(recordSource));
1579     }
1580 
checkTimerRecordingSource(int sourceType, byte[] recordSource)1581     private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) {
1582         return (recordSource != null)
1583                 && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource);
1584     }
1585 
1586     @ServiceThreadOnly
clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource)1587     void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1588         assertRunOnServiceThread();
1589         if (!mService.isControlEnabled()) {
1590             Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1591             announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_CEC_DISABLE);
1592             return;
1593         }
1594 
1595         if (!checkRecorder(recorderAddress)) {
1596             Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1597             announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION);
1598             return;
1599         }
1600 
1601         if (!checkTimerRecordingSource(sourceType, recordSource)) {
1602             Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1603             announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1604             return;
1605         }
1606 
1607         sendClearTimerMessage(recorderAddress, sourceType, recordSource);
1608     }
1609 
sendClearTimerMessage(int recorderAddress, int sourceType, byte[] recordSource)1610     private void sendClearTimerMessage(int recorderAddress, int sourceType, byte[] recordSource) {
1611         HdmiCecMessage message = null;
1612         switch (sourceType) {
1613             case TIMER_RECORDING_TYPE_DIGITAL:
1614                 message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress,
1615                         recordSource);
1616                 break;
1617             case TIMER_RECORDING_TYPE_ANALOGUE:
1618                 message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress,
1619                         recordSource);
1620                 break;
1621             case TIMER_RECORDING_TYPE_EXTERNAL:
1622                 message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress,
1623                         recordSource);
1624                 break;
1625             default:
1626                 Slog.w(TAG, "Invalid source type:" + recorderAddress);
1627                 announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1628                 return;
1629 
1630         }
1631         mService.sendCecCommand(message, new SendMessageCallback() {
1632             @Override
1633             public void onSendCompleted(int error) {
1634                 if (error != Constants.SEND_RESULT_SUCCESS) {
1635                     announceClearTimerRecordingResult(
1636                             CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1637                 }
1638             }
1639         });
1640     }
1641 
updateDevicePowerStatus(int logicalAddress, int newPowerStatus)1642     void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
1643         HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress);
1644         if (info == null) {
1645             Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress);
1646             return;
1647         }
1648 
1649         if (info.getDevicePowerStatus() == newPowerStatus) {
1650             return;
1651         }
1652 
1653         HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus);
1654         // addDeviceInfo replaces old device info with new one if exists.
1655         addDeviceInfo(newInfo);
1656 
1657         invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE);
1658     }
1659 
1660     @Override
handleMenuStatus(HdmiCecMessage message)1661     protected boolean handleMenuStatus(HdmiCecMessage message) {
1662         // Do nothing and just return true not to prevent from responding <Feature Abort>.
1663         return true;
1664     }
1665 
1666     @Override
sendStandby(int deviceId)1667     protected void sendStandby(int deviceId) {
1668         HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId);
1669         if (targetDevice == null) {
1670             return;
1671         }
1672         int targetAddress = targetDevice.getLogicalAddress();
1673         mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
1674     }
1675 
1676     @Override
dump(final IndentingPrintWriter pw)1677     protected void dump(final IndentingPrintWriter pw) {
1678         super.dump(pw);
1679         pw.println("mArcEstablished: " + mArcEstablished);
1680         pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled);
1681         pw.println("mSystemAudioActivated: " + mSystemAudioActivated);
1682         pw.println("mSystemAudioMute: " + mSystemAudioMute);
1683         pw.println("mAutoDeviceOff: " + mAutoDeviceOff);
1684         pw.println("mAutoWakeup: " + mAutoWakeup);
1685         pw.println("mSkipRoutingControl: " + mSkipRoutingControl);
1686         pw.println("CEC devices:");
1687         pw.increaseIndent();
1688         for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1689             pw.println(info);
1690         }
1691         pw.decreaseIndent();
1692     }
1693 }
1694