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