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