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