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