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