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