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