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