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