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