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