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