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