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