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