HdmiCecLocalDeviceTv.java revision 455f09d818d23d918d0146c91f977774cbfb0657
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        if (getSystemAudioModeSetting() && !isSystemAudioActivated()) {
830            addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress()));
831        }
832        if (isArcFeatureEnabled(avr.getPortId())
833                && !hasAction(SetArcTransmissionStateAction.class)) {
834            startArcAction(true);
835        }
836    }
837
838    // Clear all device info.
839    @ServiceThreadOnly
840    private void clearDeviceInfoList() {
841        assertRunOnServiceThread();
842        for (HdmiDeviceInfo info : mSafeExternalInputs) {
843            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
844        }
845        mDeviceInfos.clear();
846        updateSafeDeviceInfoList();
847    }
848
849    @ServiceThreadOnly
850    // Seq #32
851    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
852        assertRunOnServiceThread();
853        if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
854            setSystemAudioMode(false, true);
855            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
856            return;
857        }
858        HdmiDeviceInfo avr = getAvrDeviceInfo();
859        if (avr == null) {
860            setSystemAudioMode(false, true);
861            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
862            return;
863        }
864
865        addAndStartAction(
866                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
867    }
868
869    // # Seq 25
870    void setSystemAudioMode(boolean on, boolean updateSetting) {
871        HdmiLogger.debug("System Audio Mode change[old:%b new:%b]", mSystemAudioActivated, on);
872
873        if (updateSetting) {
874            mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
875        }
876        updateAudioManagerForSystemAudio(on);
877        synchronized (mLock) {
878            if (mSystemAudioActivated != on) {
879                mSystemAudioActivated = on;
880                mService.announceSystemAudioModeChange(on);
881            }
882        }
883    }
884
885    private void updateAudioManagerForSystemAudio(boolean on) {
886        int device = mService.getAudioManager().setHdmiSystemAudioSupported(on);
887        HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device);
888    }
889
890    boolean isSystemAudioActivated() {
891        if (!hasSystemAudioDevice()) {
892            return false;
893        }
894        synchronized (mLock) {
895            return mSystemAudioActivated;
896        }
897    }
898
899    boolean getSystemAudioModeSetting() {
900        return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false);
901    }
902
903    /**
904     * Change ARC status into the given {@code enabled} status.
905     *
906     * @return {@code true} if ARC was in "Enabled" status
907     */
908    @ServiceThreadOnly
909    boolean setArcStatus(boolean enabled) {
910        assertRunOnServiceThread();
911
912        HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled);
913        boolean oldStatus = mArcEstablished;
914        // 1. Enable/disable ARC circuit.
915        setAudioReturnChannel(enabled);
916        // 2. Notify arc status to audio service.
917        notifyArcStatusToAudioService(enabled);
918        // 3. Update arc status;
919        mArcEstablished = enabled;
920        return oldStatus;
921    }
922
923    /**
924     * Switch hardware ARC circuit in the system.
925     */
926    @ServiceThreadOnly
927    void setAudioReturnChannel(boolean enabled) {
928        assertRunOnServiceThread();
929        HdmiDeviceInfo avr = getAvrDeviceInfo();
930        if (avr != null) {
931            mService.setAudioReturnChannel(avr.getPortId(), enabled);
932        }
933    }
934
935    @ServiceThreadOnly
936    private void updateArcFeatureStatus(int portId, boolean isConnected) {
937        assertRunOnServiceThread();
938        HdmiPortInfo portInfo = mService.getPortInfo(portId);
939        if (!portInfo.isArcSupported()) {
940            return;
941        }
942        HdmiDeviceInfo avr = getAvrDeviceInfo();
943        if (avr == null) {
944            if (isConnected) {
945                // Update the status (since TV may not have seen AVR yet) so
946                // that ARC can be initiated after discovery.
947                mArcFeatureEnabled.put(portId, isConnected);
948            }
949            return;
950        }
951        // HEAC 2.4, HEACT 5-15
952        // Should not activate ARC if +5V status is false.
953        if (avr.getPortId() == portId) {
954            changeArcFeatureEnabled(portId, isConnected);
955        }
956    }
957
958    @ServiceThreadOnly
959    boolean isConnected(int portId) {
960        assertRunOnServiceThread();
961        return mService.isConnected(portId);
962    }
963
964    private void notifyArcStatusToAudioService(boolean enabled) {
965        // Note that we don't set any name to ARC.
966        mService.getAudioManager().setWiredDeviceConnectionState(
967                AudioSystem.DEVICE_OUT_HDMI_ARC,
968                enabled ? 1 : 0, "", "");
969    }
970
971    /**
972     * Returns true if ARC is currently established on a certain port.
973     */
974    @ServiceThreadOnly
975    boolean isArcEstablished() {
976        assertRunOnServiceThread();
977        if (mArcEstablished) {
978            for (int i = 0; i < mArcFeatureEnabled.size(); i++) {
979                if (mArcFeatureEnabled.valueAt(i)) return true;
980            }
981        }
982        return false;
983    }
984
985    @ServiceThreadOnly
986    void changeArcFeatureEnabled(int portId, boolean enabled) {
987        assertRunOnServiceThread();
988
989        if (mArcFeatureEnabled.get(portId) != enabled) {
990            mArcFeatureEnabled.put(portId, enabled);
991            if (enabled) {
992                if (!mArcEstablished) {
993                    startArcAction(true);
994                }
995            } else {
996                if (mArcEstablished) {
997                    startArcAction(false);
998                }
999            }
1000        }
1001    }
1002
1003    @ServiceThreadOnly
1004    boolean isArcFeatureEnabled(int portId) {
1005        assertRunOnServiceThread();
1006        return mArcFeatureEnabled.get(portId);
1007    }
1008
1009    @ServiceThreadOnly
1010    void startArcAction(boolean enabled) {
1011        assertRunOnServiceThread();
1012        HdmiDeviceInfo info = getAvrDeviceInfo();
1013        if (info == null) {
1014            Slog.w(TAG, "Failed to start arc action; No AVR device.");
1015            return;
1016        }
1017        if (!canStartArcUpdateAction(info.getLogicalAddress(), enabled)) {
1018            Slog.w(TAG, "Failed to start arc action; ARC configuration check failed.");
1019            if (enabled && !isConnectedToArcPort(info.getPhysicalAddress())) {
1020                displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
1021            }
1022            return;
1023        }
1024
1025        // Terminate opposite action and start action if not exist.
1026        if (enabled) {
1027            removeAction(RequestArcTerminationAction.class);
1028            if (!hasAction(RequestArcInitiationAction.class)) {
1029                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
1030            }
1031        } else {
1032            removeAction(RequestArcInitiationAction.class);
1033            if (!hasAction(RequestArcTerminationAction.class)) {
1034                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
1035            }
1036        }
1037    }
1038
1039    private boolean isDirectConnectAddress(int physicalAddress) {
1040        return (physicalAddress & Constants.ROUTING_PATH_TOP_MASK) == physicalAddress;
1041    }
1042
1043    void setAudioStatus(boolean mute, int volume) {
1044        synchronized (mLock) {
1045            mSystemAudioMute = mute;
1046            mSystemAudioVolume = volume;
1047            int maxVolume = mService.getAudioManager().getStreamMaxVolume(
1048                    AudioManager.STREAM_MUSIC);
1049            mService.setAudioStatus(mute,
1050                    VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
1051            displayOsd(HdmiControlManager.OSD_MESSAGE_AVR_VOLUME_CHANGED,
1052                    mute ? HdmiControlManager.AVR_VOLUME_MUTED : volume);
1053        }
1054    }
1055
1056    @ServiceThreadOnly
1057    void changeVolume(int curVolume, int delta, int maxVolume) {
1058        assertRunOnServiceThread();
1059        if (delta == 0 || !isSystemAudioActivated()) {
1060            return;
1061        }
1062
1063        int targetVolume = curVolume + delta;
1064        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
1065        synchronized (mLock) {
1066            // If new volume is the same as current system audio volume, just ignore it.
1067            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
1068            if (cecVolume == mSystemAudioVolume) {
1069                // Update tv volume with system volume value.
1070                mService.setAudioStatus(false,
1071                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
1072                return;
1073            }
1074        }
1075
1076        List<VolumeControlAction> actions = getActions(VolumeControlAction.class);
1077        if (actions.isEmpty()) {
1078            addAndStartAction(new VolumeControlAction(this,
1079                    getAvrDeviceInfo().getLogicalAddress(), delta > 0));
1080        } else {
1081            actions.get(0).handleVolumeChange(delta > 0);
1082        }
1083    }
1084
1085    @ServiceThreadOnly
1086    void changeMute(boolean mute) {
1087        assertRunOnServiceThread();
1088        HdmiLogger.debug("[A]:Change mute:%b", mute);
1089        synchronized (mLock) {
1090            if (mSystemAudioMute == mute) {
1091                HdmiLogger.debug("No need to change mute.");
1092                return;
1093            }
1094        }
1095        if (!isSystemAudioActivated()) {
1096            HdmiLogger.debug("[A]:System audio is not activated.");
1097            return;
1098        }
1099
1100        // Remove existing volume action.
1101        removeAction(VolumeControlAction.class);
1102        sendUserControlPressedAndReleased(getAvrDeviceInfo().getLogicalAddress(),
1103                HdmiCecKeycode.getMuteKey(mute));
1104    }
1105
1106    @Override
1107    @ServiceThreadOnly
1108    protected boolean handleInitiateArc(HdmiCecMessage message) {
1109        assertRunOnServiceThread();
1110
1111        if (!canStartArcUpdateAction(message.getSource(), true)) {
1112            if (getAvrDeviceInfo() == null) {
1113                // AVR may not have been discovered yet. Delay the message processing.
1114                mDelayedMessageBuffer.add(message);
1115                return true;
1116            }
1117            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
1118            if (!isConnectedToArcPort(message.getSource())) {
1119                displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
1120            }
1121            return true;
1122        }
1123
1124        // In case where <Initiate Arc> is started by <Request ARC Initiation>
1125        // need to clean up RequestArcInitiationAction.
1126        removeAction(RequestArcInitiationAction.class);
1127        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
1128                message.getSource(), true);
1129        addAndStartAction(action);
1130        return true;
1131    }
1132
1133    private boolean canStartArcUpdateAction(int avrAddress, boolean shouldCheckArcFeatureEnabled) {
1134        HdmiDeviceInfo avr = getAvrDeviceInfo();
1135        if (avr != null
1136                && (avrAddress == avr.getLogicalAddress())
1137                && isConnectedToArcPort(avr.getPhysicalAddress())
1138                && isDirectConnectAddress(avr.getPhysicalAddress())) {
1139            if (shouldCheckArcFeatureEnabled) {
1140                return isArcFeatureEnabled(avr.getPortId());
1141            } else {
1142                return true;
1143            }
1144        } else {
1145            return false;
1146        }
1147    }
1148
1149    @Override
1150    @ServiceThreadOnly
1151    protected boolean handleTerminateArc(HdmiCecMessage message) {
1152        assertRunOnServiceThread();
1153        if (mService .isPowerStandbyOrTransient()) {
1154            setArcStatus(false);
1155            return true;
1156        }
1157        // Do not check ARC configuration since the AVR might have been already removed.
1158        // Clean up RequestArcTerminationAction in case <Terminate Arc> was started by
1159        // <Request ARC Termination>.
1160        removeAction(RequestArcTerminationAction.class);
1161        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
1162                message.getSource(), false);
1163        addAndStartAction(action);
1164        return true;
1165    }
1166
1167    @Override
1168    @ServiceThreadOnly
1169    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
1170        assertRunOnServiceThread();
1171        if (!isMessageForSystemAudio(message)) {
1172            if (getAvrDeviceInfo() == null) {
1173                // AVR may not have been discovered yet. Delay the message processing.
1174                mDelayedMessageBuffer.add(message);
1175                return true;
1176            }
1177            HdmiLogger.warning("Invalid <Set System Audio Mode> message:" + message);
1178            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
1179            return true;
1180        }
1181        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
1182                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
1183        addAndStartAction(action);
1184        return true;
1185    }
1186
1187    @Override
1188    @ServiceThreadOnly
1189    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
1190        assertRunOnServiceThread();
1191        if (!isMessageForSystemAudio(message)) {
1192            HdmiLogger.warning("Invalid <System Audio Mode Status> message:" + message);
1193            // Ignore this message.
1194            return true;
1195        }
1196        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
1197        return true;
1198    }
1199
1200    // Seq #53
1201    @Override
1202    @ServiceThreadOnly
1203    protected boolean handleRecordTvScreen(HdmiCecMessage message) {
1204        List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
1205        if (!actions.isEmpty()) {
1206            // Assumes only one OneTouchRecordAction.
1207            OneTouchRecordAction action = actions.get(0);
1208            if (action.getRecorderAddress() != message.getSource()) {
1209                announceOneTouchRecordResult(
1210                        message.getSource(),
1211                        HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS);
1212            }
1213            return super.handleRecordTvScreen(message);
1214        }
1215
1216        int recorderAddress = message.getSource();
1217        byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress);
1218        int reason = startOneTouchRecord(recorderAddress, recordSource);
1219        if (reason != Constants.ABORT_NO_ERROR) {
1220            mService.maySendFeatureAbortCommand(message, reason);
1221        }
1222        return true;
1223    }
1224
1225    @Override
1226    protected boolean handleTimerClearedStatus(HdmiCecMessage message) {
1227        byte[] params = message.getParams();
1228        int timerClearedStatusData = params[0] & 0xFF;
1229        announceTimerRecordingResult(message.getSource(), timerClearedStatusData);
1230        return true;
1231    }
1232
1233    void announceOneTouchRecordResult(int recorderAddress, int result) {
1234        mService.invokeOneTouchRecordResult(recorderAddress, result);
1235    }
1236
1237    void announceTimerRecordingResult(int recorderAddress, int result) {
1238        mService.invokeTimerRecordingResult(recorderAddress, result);
1239    }
1240
1241    void announceClearTimerRecordingResult(int recorderAddress, int result) {
1242        mService.invokeClearTimerRecordingResult(recorderAddress, result);
1243    }
1244
1245    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
1246        return mService.isControlEnabled()
1247                && message.getSource() == Constants.ADDR_AUDIO_SYSTEM
1248                && (message.getDestination() == Constants.ADDR_TV
1249                        || message.getDestination() == Constants.ADDR_BROADCAST)
1250                && getAvrDeviceInfo() != null;
1251    }
1252
1253    /**
1254     * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
1255     * logical address as new device info's.
1256     *
1257     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1258     *
1259     * @param deviceInfo a new {@link HdmiDeviceInfo} to be added.
1260     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo}
1261     *         that has the same logical address as new one has.
1262     */
1263    @ServiceThreadOnly
1264    private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
1265        assertRunOnServiceThread();
1266        HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress());
1267        if (oldDeviceInfo != null) {
1268            removeDeviceInfo(deviceInfo.getId());
1269        }
1270        mDeviceInfos.append(deviceInfo.getId(), deviceInfo);
1271        updateSafeDeviceInfoList();
1272        return oldDeviceInfo;
1273    }
1274
1275    /**
1276     * Remove a device info corresponding to the given {@code logicalAddress}.
1277     * It returns removed {@link HdmiDeviceInfo} if exists.
1278     *
1279     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1280     *
1281     * @param id id of device to be removed
1282     * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null}
1283     */
1284    @ServiceThreadOnly
1285    private HdmiDeviceInfo removeDeviceInfo(int id) {
1286        assertRunOnServiceThread();
1287        HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id);
1288        if (deviceInfo != null) {
1289            mDeviceInfos.remove(id);
1290        }
1291        updateSafeDeviceInfoList();
1292        return deviceInfo;
1293    }
1294
1295    /**
1296     * Return a list of all {@link HdmiDeviceInfo}.
1297     *
1298     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1299     * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which
1300     * does not include local device.
1301     */
1302    @ServiceThreadOnly
1303    List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
1304        assertRunOnServiceThread();
1305        if (includeLocalDevice) {
1306            return HdmiUtils.sparseArrayToList(mDeviceInfos);
1307        } else {
1308            ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1309            for (int i = 0; i < mDeviceInfos.size(); ++i) {
1310                HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1311                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
1312                    infoList.add(info);
1313                }
1314            }
1315            return infoList;
1316        }
1317    }
1318
1319    /**
1320     * Return external input devices.
1321     */
1322    List<HdmiDeviceInfo> getSafeExternalInputsLocked() {
1323        return mSafeExternalInputs;
1324    }
1325
1326    @ServiceThreadOnly
1327    private void updateSafeDeviceInfoList() {
1328        assertRunOnServiceThread();
1329        List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
1330        List<HdmiDeviceInfo> externalInputs = getInputDevices();
1331        synchronized (mLock) {
1332            mSafeAllDeviceInfos = copiedDevices;
1333            mSafeExternalInputs = externalInputs;
1334        }
1335    }
1336
1337    /**
1338     * Return a list of external cec input (source) devices.
1339     *
1340     * <p>Note that this effectively excludes non-source devices like system audio,
1341     * secondary TV.
1342     */
1343    private List<HdmiDeviceInfo> getInputDevices() {
1344        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1345        for (int i = 0; i < mDeviceInfos.size(); ++i) {
1346            HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1347            if (isLocalDeviceAddress(info.getLogicalAddress())) {
1348                continue;
1349            }
1350            if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1351                infoList.add(info);
1352            }
1353        }
1354        return infoList;
1355    }
1356
1357    // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch.
1358    // Returns true if the policy is set to true, and the device to check does not have
1359    // a parent CEC device (which should be the CEC-enabled switch) in the list.
1360    private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) {
1361        return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH
1362                && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches);
1363    }
1364
1365    private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) {
1366        for (int switchPath : switches) {
1367            if (isParentPath(switchPath, path)) {
1368                return true;
1369            }
1370        }
1371        return false;
1372    }
1373
1374    private static boolean isParentPath(int parentPath, int childPath) {
1375        // (A000, AB00) (AB00, ABC0), (ABC0, ABCD)
1376        // If child's last non-zero nibble is removed, the result equals to the parent.
1377        for (int i = 0; i <= 12; i += 4) {
1378            int nibble = (childPath >> i) & 0xF;
1379            if (nibble != 0) {
1380                int parentNibble = (parentPath >> i) & 0xF;
1381                return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4);
1382            }
1383        }
1384        return false;
1385    }
1386
1387    private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {
1388        if (!hideDevicesBehindLegacySwitch(info)) {
1389            mService.invokeDeviceEventListeners(info, status);
1390        }
1391    }
1392
1393    private boolean isLocalDeviceAddress(int address) {
1394        return mLocalDeviceAddresses.contains(address);
1395    }
1396
1397    @ServiceThreadOnly
1398    HdmiDeviceInfo getAvrDeviceInfo() {
1399        assertRunOnServiceThread();
1400        return getCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1401    }
1402
1403    /**
1404     * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}.
1405     *
1406     * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}.
1407     *
1408     * @param logicalAddress logical address of the device to be retrieved
1409     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1410     *         Returns null if no logical address matched
1411     */
1412    @ServiceThreadOnly
1413    HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) {
1414        assertRunOnServiceThread();
1415        return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
1416    }
1417
1418    boolean hasSystemAudioDevice() {
1419        return getSafeAvrDeviceInfo() != null;
1420    }
1421
1422    HdmiDeviceInfo getSafeAvrDeviceInfo() {
1423        return getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1424    }
1425
1426    /**
1427     * Thread safe version of {@link #getCecDeviceInfo(int)}.
1428     *
1429     * @param logicalAddress logical address to be retrieved
1430     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1431     *         Returns null if no logical address matched
1432     */
1433    HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) {
1434        synchronized (mLock) {
1435            for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1436                if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) {
1437                    return info;
1438                }
1439            }
1440            return null;
1441        }
1442    }
1443
1444    List<HdmiDeviceInfo> getSafeCecDevicesLocked() {
1445        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1446        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1447            if (isLocalDeviceAddress(info.getLogicalAddress())) {
1448                continue;
1449            }
1450            infoList.add(info);
1451        }
1452        return infoList;
1453    }
1454
1455    /**
1456     * Called when a device is newly added or a new device is detected or
1457     * existing device is updated.
1458     *
1459     * @param info device info of a new device.
1460     */
1461    @ServiceThreadOnly
1462    final void addCecDevice(HdmiDeviceInfo info) {
1463        assertRunOnServiceThread();
1464        HdmiDeviceInfo old = addDeviceInfo(info);
1465        if (info.getLogicalAddress() == mAddress) {
1466            // The addition of TV device itself should not be notified.
1467            return;
1468        }
1469        if (old == null) {
1470            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
1471        } else if (!old.equals(info)) {
1472            invokeDeviceEventListener(old, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
1473            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
1474        }
1475    }
1476
1477    /**
1478     * Called when a device is removed or removal of device is detected.
1479     *
1480     * @param address a logical address of a device to be removed
1481     */
1482    @ServiceThreadOnly
1483    final void removeCecDevice(int address) {
1484        assertRunOnServiceThread();
1485        HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
1486
1487        mCecMessageCache.flushMessagesFrom(address);
1488        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
1489    }
1490
1491    @ServiceThreadOnly
1492    void handleRemoveActiveRoutingPath(int path) {
1493        assertRunOnServiceThread();
1494        // Seq #23
1495        if (isTailOfActivePath(path, getActivePath())) {
1496            int newPath = mService.portIdToPath(getActivePortId());
1497            startRoutingControl(getActivePath(), newPath, true, null);
1498        }
1499    }
1500
1501    /**
1502     * Launch routing control process.
1503     *
1504     * @param routingForBootup true if routing control is initiated due to One Touch Play
1505     *        or TV power on
1506     */
1507    @ServiceThreadOnly
1508    void launchRoutingControl(boolean routingForBootup) {
1509        assertRunOnServiceThread();
1510        // Seq #24
1511        if (getActivePortId() != Constants.INVALID_PORT_ID) {
1512            if (!routingForBootup && !isProhibitMode()) {
1513                int newPath = mService.portIdToPath(getActivePortId());
1514                setActivePath(newPath);
1515                startRoutingControl(getActivePath(), newPath, routingForBootup, null);
1516            }
1517        } else {
1518            int activePath = mService.getPhysicalAddress();
1519            setActivePath(activePath);
1520            if (!routingForBootup
1521                    && !mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) {
1522                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1523                        activePath));
1524            }
1525        }
1526    }
1527
1528    /**
1529     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
1530     * the given routing path. CEC devices use routing path for its physical address to
1531     * describe the hierarchy of the devices in the network.
1532     *
1533     * @param path routing path or physical address
1534     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
1535     */
1536    @ServiceThreadOnly
1537    final HdmiDeviceInfo getDeviceInfoByPath(int path) {
1538        assertRunOnServiceThread();
1539        for (HdmiDeviceInfo info : getDeviceInfoList(false)) {
1540            if (info.getPhysicalAddress() == path) {
1541                return info;
1542            }
1543        }
1544        return null;
1545    }
1546
1547    /**
1548     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
1549     * the given routing path. This is the version accessible safely from threads
1550     * other than service thread.
1551     *
1552     * @param path routing path or physical address
1553     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
1554     */
1555    HdmiDeviceInfo getSafeDeviceInfoByPath(int path) {
1556        synchronized (mLock) {
1557            for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1558                if (info.getPhysicalAddress() == path) {
1559                    return info;
1560                }
1561            }
1562            return null;
1563        }
1564    }
1565
1566    /**
1567     * Whether a device of the specified physical address and logical address exists
1568     * in a device info list. However, both are minimal condition and it could
1569     * be different device from the original one.
1570     *
1571     * @param logicalAddress logical address of a device to be searched
1572     * @param physicalAddress physical address of a device to be searched
1573     * @return true if exist; otherwise false
1574     */
1575    @ServiceThreadOnly
1576    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1577        assertRunOnServiceThread();
1578        HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress);
1579        if (device == null) {
1580            return false;
1581        }
1582        return device.getPhysicalAddress() == physicalAddress;
1583    }
1584
1585    @Override
1586    @ServiceThreadOnly
1587    void onHotplug(int portId, boolean connected) {
1588        assertRunOnServiceThread();
1589
1590        if (!connected) {
1591            removeCecSwitches(portId);
1592        }
1593        // Tv device will have permanent HotplugDetectionAction.
1594        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1595        if (!hotplugActions.isEmpty()) {
1596            // Note that hotplug action is single action running on a machine.
1597            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1598            // It covers seq #40, #43.
1599            hotplugActions.get(0).pollAllDevicesNow();
1600        }
1601        updateArcFeatureStatus(portId, connected);
1602    }
1603
1604    private void removeCecSwitches(int portId) {
1605        Iterator<Integer> it = mCecSwitches.iterator();
1606        while (!it.hasNext()) {
1607            int path = it.next();
1608            if (pathToPortId(path) == portId) {
1609                it.remove();
1610            }
1611        }
1612    }
1613
1614    @Override
1615    @ServiceThreadOnly
1616    void setAutoDeviceOff(boolean enabled) {
1617        assertRunOnServiceThread();
1618        mAutoDeviceOff = enabled;
1619    }
1620
1621    @ServiceThreadOnly
1622    void setAutoWakeup(boolean enabled) {
1623        assertRunOnServiceThread();
1624        mAutoWakeup = enabled;
1625    }
1626
1627    @ServiceThreadOnly
1628    boolean getAutoWakeup() {
1629        assertRunOnServiceThread();
1630        return mAutoWakeup;
1631    }
1632
1633    @Override
1634    @ServiceThreadOnly
1635    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1636        assertRunOnServiceThread();
1637        mService.unregisterTvInputCallback(mTvInputCallback);
1638        // Remove any repeated working actions.
1639        // HotplugDetectionAction will be reinstated during the wake up process.
1640        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1641        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1642        removeAction(DeviceDiscoveryAction.class);
1643        removeAction(HotplugDetectionAction.class);
1644        removeAction(PowerStatusMonitorAction.class);
1645        // Remove recording actions.
1646        removeAction(OneTouchRecordAction.class);
1647        removeAction(TimerRecordingAction.class);
1648
1649        disableSystemAudioIfExist();
1650        disableArcIfExist();
1651
1652        super.disableDevice(initiatedByCec, callback);
1653        clearDeviceInfoList();
1654        getActiveSource().invalidate();
1655        setActivePath(Constants.INVALID_PHYSICAL_ADDRESS);
1656        checkIfPendingActionsCleared();
1657    }
1658
1659    @ServiceThreadOnly
1660    private void disableSystemAudioIfExist() {
1661        assertRunOnServiceThread();
1662        if (getAvrDeviceInfo() == null) {
1663            return;
1664        }
1665
1666        // Seq #31.
1667        removeAction(SystemAudioActionFromAvr.class);
1668        removeAction(SystemAudioActionFromTv.class);
1669        removeAction(SystemAudioAutoInitiationAction.class);
1670        removeAction(SystemAudioStatusAction.class);
1671        removeAction(VolumeControlAction.class);
1672    }
1673
1674    @ServiceThreadOnly
1675    private void disableArcIfExist() {
1676        assertRunOnServiceThread();
1677        HdmiDeviceInfo avr = getAvrDeviceInfo();
1678        if (avr == null) {
1679            return;
1680        }
1681
1682        // Seq #44.
1683        removeAction(RequestArcInitiationAction.class);
1684        if (!hasAction(RequestArcTerminationAction.class) && isArcEstablished()) {
1685            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1686        }
1687    }
1688
1689    @Override
1690    @ServiceThreadOnly
1691    protected void onStandby(boolean initiatedByCec, int standbyAction) {
1692        assertRunOnServiceThread();
1693        // Seq #11
1694        if (!mService.isControlEnabled()) {
1695            return;
1696        }
1697        if (!initiatedByCec && mAutoDeviceOff) {
1698            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1699                    mAddress, Constants.ADDR_BROADCAST));
1700        }
1701    }
1702
1703    boolean isProhibitMode() {
1704        return mService.isProhibitMode();
1705    }
1706
1707    boolean isPowerStandbyOrTransient() {
1708        return mService.isPowerStandbyOrTransient();
1709    }
1710
1711    @ServiceThreadOnly
1712    void displayOsd(int messageId) {
1713        assertRunOnServiceThread();
1714        mService.displayOsd(messageId);
1715    }
1716
1717    @ServiceThreadOnly
1718    void displayOsd(int messageId, int extra) {
1719        assertRunOnServiceThread();
1720        mService.displayOsd(messageId, extra);
1721    }
1722
1723    // Seq #54 and #55
1724    @ServiceThreadOnly
1725    int startOneTouchRecord(int recorderAddress, byte[] recordSource) {
1726        assertRunOnServiceThread();
1727        if (!mService.isControlEnabled()) {
1728            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1729            announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED);
1730            return Constants.ABORT_NOT_IN_CORRECT_MODE;
1731        }
1732
1733        if (!checkRecorder(recorderAddress)) {
1734            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1735            announceOneTouchRecordResult(recorderAddress,
1736                    ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1737            return Constants.ABORT_NOT_IN_CORRECT_MODE;
1738        }
1739
1740        if (!checkRecordSource(recordSource)) {
1741            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1742            announceOneTouchRecordResult(recorderAddress,
1743                    ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN);
1744            return Constants.ABORT_CANNOT_PROVIDE_SOURCE;
1745        }
1746
1747        addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource));
1748        Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:"
1749                + Arrays.toString(recordSource));
1750        return Constants.ABORT_NO_ERROR;
1751    }
1752
1753    @ServiceThreadOnly
1754    void stopOneTouchRecord(int recorderAddress) {
1755        assertRunOnServiceThread();
1756        if (!mService.isControlEnabled()) {
1757            Slog.w(TAG, "Can not stop one touch record. CEC control is disabled.");
1758            announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED);
1759            return;
1760        }
1761
1762        if (!checkRecorder(recorderAddress)) {
1763            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1764            announceOneTouchRecordResult(recorderAddress,
1765                    ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1766            return;
1767        }
1768
1769        // Remove one touch record action so that other one touch record can be started.
1770        removeAction(OneTouchRecordAction.class);
1771        mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress));
1772        Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress);
1773    }
1774
1775    private boolean checkRecorder(int recorderAddress) {
1776        HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress);
1777        return (device != null)
1778                && (HdmiUtils.getTypeFromAddress(recorderAddress)
1779                        == HdmiDeviceInfo.DEVICE_RECORDER);
1780    }
1781
1782    private boolean checkRecordSource(byte[] recordSource) {
1783        return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource);
1784    }
1785
1786    @ServiceThreadOnly
1787    void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1788        assertRunOnServiceThread();
1789        if (!mService.isControlEnabled()) {
1790            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1791            announceTimerRecordingResult(recorderAddress,
1792                    TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED);
1793            return;
1794        }
1795
1796        if (!checkRecorder(recorderAddress)) {
1797            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1798            announceTimerRecordingResult(recorderAddress,
1799                    TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION);
1800            return;
1801        }
1802
1803        if (!checkTimerRecordingSource(sourceType, recordSource)) {
1804            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1805            announceTimerRecordingResult(
1806                    recorderAddress,
1807                    TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE);
1808            return;
1809        }
1810
1811        addAndStartAction(
1812                new TimerRecordingAction(this, recorderAddress, sourceType, recordSource));
1813        Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:"
1814                + sourceType + ", RecordSource:" + Arrays.toString(recordSource));
1815    }
1816
1817    private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) {
1818        return (recordSource != null)
1819                && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource);
1820    }
1821
1822    @ServiceThreadOnly
1823    void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1824        assertRunOnServiceThread();
1825        if (!mService.isControlEnabled()) {
1826            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1827            announceClearTimerRecordingResult(recorderAddress, CLEAR_TIMER_STATUS_CEC_DISABLE);
1828            return;
1829        }
1830
1831        if (!checkRecorder(recorderAddress)) {
1832            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1833            announceClearTimerRecordingResult(recorderAddress,
1834                    CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION);
1835            return;
1836        }
1837
1838        if (!checkTimerRecordingSource(sourceType, recordSource)) {
1839            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1840            announceClearTimerRecordingResult(recorderAddress,
1841                    CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1842            return;
1843        }
1844
1845        sendClearTimerMessage(recorderAddress, sourceType, recordSource);
1846    }
1847
1848    private void sendClearTimerMessage(final int recorderAddress, int sourceType,
1849            byte[] recordSource) {
1850        HdmiCecMessage message = null;
1851        switch (sourceType) {
1852            case TIMER_RECORDING_TYPE_DIGITAL:
1853                message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress,
1854                        recordSource);
1855                break;
1856            case TIMER_RECORDING_TYPE_ANALOGUE:
1857                message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress,
1858                        recordSource);
1859                break;
1860            case TIMER_RECORDING_TYPE_EXTERNAL:
1861                message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress,
1862                        recordSource);
1863                break;
1864            default:
1865                Slog.w(TAG, "Invalid source type:" + recorderAddress);
1866                announceClearTimerRecordingResult(recorderAddress,
1867                        CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1868                return;
1869
1870        }
1871        mService.sendCecCommand(message, new SendMessageCallback() {
1872            @Override
1873            public void onSendCompleted(int error) {
1874                if (error != Constants.SEND_RESULT_SUCCESS) {
1875                    announceClearTimerRecordingResult(recorderAddress,
1876                            CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1877                }
1878            }
1879        });
1880    }
1881
1882    void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
1883        HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress);
1884        if (info == null) {
1885            Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress);
1886            return;
1887        }
1888
1889        if (info.getDevicePowerStatus() == newPowerStatus) {
1890            return;
1891        }
1892
1893        HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus);
1894        // addDeviceInfo replaces old device info with new one if exists.
1895        addDeviceInfo(newInfo);
1896
1897        invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE);
1898    }
1899
1900    @Override
1901    protected boolean handleMenuStatus(HdmiCecMessage message) {
1902        // Do nothing and just return true not to prevent from responding <Feature Abort>.
1903        return true;
1904    }
1905
1906    @Override
1907    protected void sendStandby(int deviceId) {
1908        HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId);
1909        if (targetDevice == null) {
1910            return;
1911        }
1912        int targetAddress = targetDevice.getLogicalAddress();
1913        mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
1914    }
1915
1916    @ServiceThreadOnly
1917    void processAllDelayedMessages() {
1918        assertRunOnServiceThread();
1919        mDelayedMessageBuffer.processAllMessages();
1920    }
1921
1922    @ServiceThreadOnly
1923    void processDelayedMessages(int address) {
1924        assertRunOnServiceThread();
1925        mDelayedMessageBuffer.processMessagesForDevice(address);
1926    }
1927
1928    @ServiceThreadOnly
1929    void processDelayedActiveSource(int address) {
1930        assertRunOnServiceThread();
1931        mDelayedMessageBuffer.processActiveSource(address);
1932    }
1933
1934    @Override
1935    protected void dump(final IndentingPrintWriter pw) {
1936        super.dump(pw);
1937        pw.println("mArcEstablished: " + mArcEstablished);
1938        pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled);
1939        pw.println("mSystemAudioActivated: " + mSystemAudioActivated);
1940        pw.println("mSystemAudioMute: " + mSystemAudioMute);
1941        pw.println("mAutoDeviceOff: " + mAutoDeviceOff);
1942        pw.println("mAutoWakeup: " + mAutoWakeup);
1943        pw.println("mSkipRoutingControl: " + mSkipRoutingControl);
1944        pw.println("mPrevPortId: " + mPrevPortId);
1945        pw.println("CEC devices:");
1946        pw.increaseIndent();
1947        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1948            pw.println(info);
1949        }
1950        pw.decreaseIndent();
1951    }
1952}
1953