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