HdmiCecLocalDeviceTv.java revision bdf27fbf746bee11430c4db2ea6dfd026bae77fe
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        if (getSystemAudioModeSetting()) {
710            addAndStartAction(new SystemAudioAutoInitiationAction(this, avr.getLogicalAddress()));
711        }
712        if (isArcFeatureEnabled()) {
713            startArcAction(true);
714        }
715    }
716
717    // Clear all device info.
718    @ServiceThreadOnly
719    private void clearDeviceInfoList() {
720        assertRunOnServiceThread();
721        for (HdmiDeviceInfo info : mSafeExternalInputs) {
722            invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
723        }
724        mDeviceInfos.clear();
725        updateSafeDeviceInfoList();
726    }
727
728    @ServiceThreadOnly
729    // Seq #32
730    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
731        assertRunOnServiceThread();
732        if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
733            setSystemAudioMode(false, true);
734            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
735            return;
736        }
737        HdmiDeviceInfo avr = getAvrDeviceInfo();
738        if (avr == null) {
739            setSystemAudioMode(false, true);
740            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
741            return;
742        }
743
744        addAndStartAction(
745                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
746    }
747
748    // # Seq 25
749    void setSystemAudioMode(boolean on, boolean updateSetting) {
750        HdmiLogger.debug("System Audio Mode change[old:%b new:%b]", mSystemAudioActivated, on);
751
752        if (updateSetting) {
753            mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
754        }
755        updateAudioManagerForSystemAudio(on);
756        synchronized (mLock) {
757            if (mSystemAudioActivated != on) {
758                mSystemAudioActivated = on;
759                mService.announceSystemAudioModeChange(on);
760            }
761        }
762    }
763
764    private void updateAudioManagerForSystemAudio(boolean on) {
765        int device = mService.getAudioManager().setHdmiSystemAudioSupported(on);
766        HdmiLogger.debug("[A]UpdateSystemAudio mode[on=%b] output=[%X]", on, device);
767    }
768
769    boolean isSystemAudioActivated() {
770        if (getAvrDeviceInfo() == null) {
771            return false;
772        }
773        synchronized (mLock) {
774            return mSystemAudioActivated;
775        }
776    }
777
778    boolean getSystemAudioModeSetting() {
779        return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false);
780    }
781
782    /**
783     * Change ARC status into the given {@code enabled} status.
784     *
785     * @return {@code true} if ARC was in "Enabled" status
786     */
787    @ServiceThreadOnly
788    boolean setArcStatus(boolean enabled) {
789        assertRunOnServiceThread();
790
791        HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled);
792        boolean oldStatus = mArcEstablished;
793        // 1. Enable/disable ARC circuit.
794        mService.setAudioReturnChannel(enabled);
795        // 2. Notify arc status to audio service.
796        notifyArcStatusToAudioService(enabled);
797        // 3. Update arc status;
798        mArcEstablished = enabled;
799        return oldStatus;
800    }
801
802    private void notifyArcStatusToAudioService(boolean enabled) {
803        // Note that we don't set any name to ARC.
804        mService.getAudioManager().setWiredDeviceConnectionState(
805                AudioSystem.DEVICE_OUT_HDMI_ARC,
806                enabled ? 1 : 0, "");
807    }
808
809    /**
810     * Returns whether ARC is enabled or not.
811     */
812    @ServiceThreadOnly
813    boolean isArcEstabilished() {
814        assertRunOnServiceThread();
815        return mArcFeatureEnabled && mArcEstablished;
816    }
817
818    @ServiceThreadOnly
819    void changeArcFeatureEnabled(boolean enabled) {
820        assertRunOnServiceThread();
821
822        if (mArcFeatureEnabled != enabled) {
823            mArcFeatureEnabled = enabled;
824            if (enabled) {
825                if (!mArcEstablished) {
826                    startArcAction(true);
827                }
828            } else {
829                if (mArcEstablished) {
830                    startArcAction(false);
831                }
832            }
833        }
834    }
835
836    @ServiceThreadOnly
837    boolean isArcFeatureEnabled() {
838        assertRunOnServiceThread();
839        return mArcFeatureEnabled;
840    }
841
842    @ServiceThreadOnly
843    void startArcAction(boolean enabled) {
844        assertRunOnServiceThread();
845        HdmiDeviceInfo info = getAvrDeviceInfo();
846        if (info == null) {
847            Slog.w(TAG, "Failed to start arc action; No AVR device.");
848            return;
849        }
850        if (!canStartArcUpdateAction(info.getLogicalAddress(), enabled)) {
851            Slog.w(TAG, "Failed to start arc action; ARC configuration check failed.");
852            if (enabled && !isConnectedToArcPort(info.getPhysicalAddress())) {
853                displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
854            }
855            return;
856        }
857
858        // Terminate opposite action and start action if not exist.
859        if (enabled) {
860            removeAction(RequestArcTerminationAction.class);
861            if (!hasAction(RequestArcInitiationAction.class)) {
862                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
863            }
864        } else {
865            removeAction(RequestArcInitiationAction.class);
866            if (!hasAction(RequestArcTerminationAction.class)) {
867                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
868            }
869        }
870    }
871
872    private boolean isDirectConnectAddress(int physicalAddress) {
873        return (physicalAddress & Constants.ROUTING_PATH_TOP_MASK) == physicalAddress;
874    }
875
876    void setAudioStatus(boolean mute, int volume) {
877        synchronized (mLock) {
878            mSystemAudioMute = mute;
879            mSystemAudioVolume = volume;
880            int maxVolume = mService.getAudioManager().getStreamMaxVolume(
881                    AudioManager.STREAM_MUSIC);
882            mService.setAudioStatus(mute,
883                    VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
884            displayOsd(HdmiControlManager.OSD_MESSAGE_AVR_VOLUME_CHANGED,
885                    mute ? HdmiControlManager.AVR_VOLUME_MUTED : volume);
886        }
887    }
888
889    @ServiceThreadOnly
890    void changeVolume(int curVolume, int delta, int maxVolume) {
891        assertRunOnServiceThread();
892        if (delta == 0 || !isSystemAudioActivated()) {
893            return;
894        }
895
896        int targetVolume = curVolume + delta;
897        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
898        synchronized (mLock) {
899            // If new volume is the same as current system audio volume, just ignore it.
900            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
901            if (cecVolume == mSystemAudioVolume) {
902                // Update tv volume with system volume value.
903                mService.setAudioStatus(false,
904                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
905                return;
906            }
907        }
908
909        List<VolumeControlAction> actions = getActions(VolumeControlAction.class);
910        if (actions.isEmpty()) {
911            addAndStartAction(new VolumeControlAction(this,
912                    getAvrDeviceInfo().getLogicalAddress(), delta > 0));
913        } else {
914            actions.get(0).handleVolumeChange(delta > 0);
915        }
916    }
917
918    @ServiceThreadOnly
919    void changeMute(boolean mute) {
920        assertRunOnServiceThread();
921        HdmiLogger.debug("[A]:Change mute:%b", mute);
922        synchronized (mLock) {
923            if (mSystemAudioMute == mute) {
924                HdmiLogger.debug("No need to change mute.");
925                return;
926            }
927        }
928        if (!isSystemAudioActivated()) {
929            HdmiLogger.debug("[A]:System audio is not activated.");
930            return;
931        }
932
933        // Remove existing volume action.
934        removeAction(VolumeControlAction.class);
935        sendUserControlPressedAndReleased(getAvrDeviceInfo().getLogicalAddress(),
936                mute ? HdmiCecKeycode.CEC_KEYCODE_MUTE_FUNCTION :
937                        HdmiCecKeycode.CEC_KEYCODE_RESTORE_VOLUME_FUNCTION);
938    }
939
940    @Override
941    @ServiceThreadOnly
942    protected boolean handleInitiateArc(HdmiCecMessage message) {
943        assertRunOnServiceThread();
944
945        if (!canStartArcUpdateAction(message.getSource(), true)) {
946            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
947            if (!isConnectedToArcPort(message.getSource())) {
948                displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
949            }
950            return true;
951        }
952
953        // In case where <Initiate Arc> is started by <Request ARC Initiation>
954        // need to clean up RequestArcInitiationAction.
955        removeAction(RequestArcInitiationAction.class);
956        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
957                message.getSource(), true);
958        addAndStartAction(action);
959        return true;
960    }
961
962    private boolean canStartArcUpdateAction(int avrAddress, boolean shouldCheckArcFeatureEnabled) {
963        HdmiDeviceInfo avr = getAvrDeviceInfo();
964        if (avr != null
965                && (avrAddress == avr.getLogicalAddress())
966                && isConnectedToArcPort(avr.getPhysicalAddress())
967                && isDirectConnectAddress(avr.getPhysicalAddress())) {
968            if (shouldCheckArcFeatureEnabled) {
969                return isArcFeatureEnabled();
970            } else {
971                return true;
972            }
973        } else {
974            return false;
975        }
976    }
977
978    @Override
979    @ServiceThreadOnly
980    protected boolean handleTerminateArc(HdmiCecMessage message) {
981        assertRunOnServiceThread();
982        // In cast of termination, do not check ARC configuration in that AVR device
983        // might be removed already.
984
985        // In case where <Terminate Arc> is started by <Request ARC Termination>
986        // need to clean up RequestArcInitiationAction.
987        removeAction(RequestArcTerminationAction.class);
988        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
989                message.getSource(), false);
990        addAndStartAction(action);
991        return true;
992    }
993
994    @Override
995    @ServiceThreadOnly
996    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
997        assertRunOnServiceThread();
998        if (!isMessageForSystemAudio(message)) {
999            HdmiLogger.warning("Invalid <Set System Audio Mode> message:" + message);
1000            mService.maySendFeatureAbortCommand(message, Constants.ABORT_REFUSED);
1001            return true;
1002        }
1003        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
1004                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
1005        addAndStartAction(action);
1006        return true;
1007    }
1008
1009    @Override
1010    @ServiceThreadOnly
1011    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
1012        assertRunOnServiceThread();
1013        if (!isMessageForSystemAudio(message)) {
1014            HdmiLogger.warning("Invalid <System Audio Mode Status> message:" + message);
1015            // Ignore this message.
1016            return true;
1017        }
1018        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
1019        return true;
1020    }
1021
1022    // Seq #53
1023    @Override
1024    @ServiceThreadOnly
1025    protected boolean handleRecordTvScreen(HdmiCecMessage message) {
1026        List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
1027        if (!actions.isEmpty()) {
1028            // Assumes only one OneTouchRecordAction.
1029            OneTouchRecordAction action = actions.get(0);
1030            if (action.getRecorderAddress() != message.getSource()) {
1031                announceOneTouchRecordResult(
1032                        HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS);
1033            }
1034            return super.handleRecordTvScreen(message);
1035        }
1036
1037        int recorderAddress = message.getSource();
1038        byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress);
1039        int reason = startOneTouchRecord(recorderAddress, recordSource);
1040        if (reason != Constants.ABORT_NO_ERROR) {
1041            mService.maySendFeatureAbortCommand(message, reason);
1042        }
1043        return true;
1044    }
1045
1046    @Override
1047    protected boolean handleTimerClearedStatus(HdmiCecMessage message) {
1048        byte[] params = message.getParams();
1049        int timerClearedStatusData = params[0] & 0xFF;
1050        announceTimerRecordingResult(timerClearedStatusData);
1051        return true;
1052    }
1053
1054    void announceOneTouchRecordResult(int result) {
1055        mService.invokeOneTouchRecordResult(result);
1056    }
1057
1058    void announceTimerRecordingResult(int result) {
1059        mService.invokeTimerRecordingResult(result);
1060    }
1061
1062    void announceClearTimerRecordingResult(int result) {
1063        mService.invokeClearTimerRecordingResult(result);
1064    }
1065
1066    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
1067        return mService.isControlEnabled()
1068                && message.getSource() == Constants.ADDR_AUDIO_SYSTEM
1069                && (message.getDestination() == Constants.ADDR_TV
1070                        || message.getDestination() == Constants.ADDR_BROADCAST)
1071                && getAvrDeviceInfo() != null;
1072    }
1073
1074    /**
1075     * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
1076     * logical address as new device info's.
1077     *
1078     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1079     *
1080     * @param deviceInfo a new {@link HdmiDeviceInfo} to be added.
1081     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo}
1082     *         that has the same logical address as new one has.
1083     */
1084    @ServiceThreadOnly
1085    private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
1086        assertRunOnServiceThread();
1087        HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress());
1088        if (oldDeviceInfo != null) {
1089            removeDeviceInfo(deviceInfo.getId());
1090        }
1091        mDeviceInfos.append(deviceInfo.getId(), deviceInfo);
1092        updateSafeDeviceInfoList();
1093        return oldDeviceInfo;
1094    }
1095
1096    /**
1097     * Remove a device info corresponding to the given {@code logicalAddress}.
1098     * It returns removed {@link HdmiDeviceInfo} if exists.
1099     *
1100     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1101     *
1102     * @param id id of device to be removed
1103     * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null}
1104     */
1105    @ServiceThreadOnly
1106    private HdmiDeviceInfo removeDeviceInfo(int id) {
1107        assertRunOnServiceThread();
1108        HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id);
1109        if (deviceInfo != null) {
1110            mDeviceInfos.remove(id);
1111        }
1112        updateSafeDeviceInfoList();
1113        return deviceInfo;
1114    }
1115
1116    /**
1117     * Return a list of all {@link HdmiDeviceInfo}.
1118     *
1119     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1120     * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which
1121     * does not include local device.
1122     */
1123    @ServiceThreadOnly
1124    List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
1125        assertRunOnServiceThread();
1126        if (includeLocalDevice) {
1127            return HdmiUtils.sparseArrayToList(mDeviceInfos);
1128        } else {
1129            ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1130            for (int i = 0; i < mDeviceInfos.size(); ++i) {
1131                HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1132                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
1133                    infoList.add(info);
1134                }
1135            }
1136            return infoList;
1137        }
1138    }
1139
1140    /**
1141     * Return external input devices.
1142     */
1143    List<HdmiDeviceInfo> getSafeExternalInputsLocked() {
1144        return mSafeExternalInputs;
1145    }
1146
1147    @ServiceThreadOnly
1148    private void updateSafeDeviceInfoList() {
1149        assertRunOnServiceThread();
1150        List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
1151        List<HdmiDeviceInfo> externalInputs = getInputDevices();
1152        synchronized (mLock) {
1153            mSafeAllDeviceInfos = copiedDevices;
1154            mSafeExternalInputs = externalInputs;
1155        }
1156    }
1157
1158    /**
1159     * Return a list of external cec input (source) devices.
1160     *
1161     * <p>Note that this effectively excludes non-source devices like system audio,
1162     * secondary TV.
1163     */
1164    private List<HdmiDeviceInfo> getInputDevices() {
1165        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1166        for (int i = 0; i < mDeviceInfos.size(); ++i) {
1167            HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1168            if (isLocalDeviceAddress(info.getLogicalAddress())) {
1169                continue;
1170            }
1171            if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1172                infoList.add(info);
1173            }
1174        }
1175        return infoList;
1176    }
1177
1178    // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch.
1179    // Returns true if the policy is set to true, and the device to check does not have
1180    // a parent CEC device (which should be the CEC-enabled switch) in the list.
1181    private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) {
1182        return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH
1183                && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches);
1184    }
1185
1186    private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) {
1187        for (int switchPath : switches) {
1188            if (isParentPath(switchPath, path)) {
1189                return true;
1190            }
1191        }
1192        return false;
1193    }
1194
1195    private static boolean isParentPath(int parentPath, int childPath) {
1196        // (A000, AB00) (AB00, ABC0), (ABC0, ABCD)
1197        // If child's last non-zero nibble is removed, the result equals to the parent.
1198        for (int i = 0; i <= 12; i += 4) {
1199            int nibble = (childPath >> i) & 0xF;
1200            if (nibble != 0) {
1201                int parentNibble = (parentPath >> i) & 0xF;
1202                return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4);
1203            }
1204        }
1205        return false;
1206    }
1207
1208    private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {
1209        if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1210            mService.invokeDeviceEventListeners(info, status);
1211        }
1212    }
1213
1214    private boolean isLocalDeviceAddress(int address) {
1215        return mLocalDeviceAddresses.contains(address);
1216    }
1217
1218    @ServiceThreadOnly
1219    HdmiDeviceInfo getAvrDeviceInfo() {
1220        assertRunOnServiceThread();
1221        return getCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1222    }
1223
1224    /**
1225     * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}.
1226     *
1227     * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}.
1228     *
1229     * @param logicalAddress logical address of the device to be retrieved
1230     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1231     *         Returns null if no logical address matched
1232     */
1233    @ServiceThreadOnly
1234    HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) {
1235        assertRunOnServiceThread();
1236        return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
1237    }
1238
1239    boolean hasSystemAudioDevice() {
1240        return getSafeAvrDeviceInfo() != null;
1241    }
1242
1243    HdmiDeviceInfo getSafeAvrDeviceInfo() {
1244        return getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1245    }
1246
1247    /**
1248     * Thread safe version of {@link #getCecDeviceInfo(int)}.
1249     *
1250     * @param logicalAddress logical address to be retrieved
1251     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1252     *         Returns null if no logical address matched
1253     */
1254    HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) {
1255        synchronized (mLock) {
1256            for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1257                if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) {
1258                    return info;
1259                }
1260            }
1261            return null;
1262        }
1263    }
1264
1265    List<HdmiDeviceInfo> getSafeCecDevicesLocked() {
1266        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1267        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1268            if (isLocalDeviceAddress(info.getLogicalAddress())) {
1269                continue;
1270            }
1271            infoList.add(info);
1272        }
1273        return infoList;
1274    }
1275
1276    /**
1277     * Called when a device is newly added or a new device is detected or
1278     * existing device is updated.
1279     *
1280     * @param info device info of a new device.
1281     */
1282    @ServiceThreadOnly
1283    final void addCecDevice(HdmiDeviceInfo info) {
1284        assertRunOnServiceThread();
1285        addDeviceInfo(info);
1286        if (info.getLogicalAddress() == mAddress) {
1287            // The addition of TV device itself should not be notified.
1288            return;
1289        }
1290        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
1291    }
1292
1293    /**
1294     * Called when a device is removed or removal of device is detected.
1295     *
1296     * @param address a logical address of a device to be removed
1297     */
1298    @ServiceThreadOnly
1299    final void removeCecDevice(int address) {
1300        assertRunOnServiceThread();
1301        HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
1302
1303        mCecMessageCache.flushMessagesFrom(address);
1304        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
1305    }
1306
1307    @ServiceThreadOnly
1308    void handleRemoveActiveRoutingPath(int path) {
1309        assertRunOnServiceThread();
1310        // Seq #23
1311        if (isTailOfActivePath(path, getActivePath())) {
1312            removeAction(RoutingControlAction.class);
1313            int newPath = mService.portIdToPath(getActivePortId());
1314            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
1315                    mAddress, getActivePath(), newPath));
1316            mActiveSource.invalidate();
1317            addAndStartAction(new RoutingControlAction(this, getActivePath(), true, null));
1318        }
1319    }
1320
1321    /**
1322     * Launch routing control process.
1323     *
1324     * @param routingForBootup true if routing control is initiated due to One Touch Play
1325     *        or TV power on
1326     */
1327    @ServiceThreadOnly
1328    void launchRoutingControl(boolean routingForBootup) {
1329        assertRunOnServiceThread();
1330        // Seq #24
1331        if (getActivePortId() != Constants.INVALID_PORT_ID) {
1332            if (!routingForBootup && !isProhibitMode()) {
1333                removeAction(RoutingControlAction.class);
1334                int newPath = mService.portIdToPath(getActivePortId());
1335                setActivePath(newPath);
1336                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
1337                        getActivePath(), newPath));
1338                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1339                        routingForBootup, null));
1340            }
1341        } else {
1342            int activePath = mService.getPhysicalAddress();
1343            setActivePath(activePath);
1344            if (!routingForBootup) {
1345                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1346                        activePath));
1347            }
1348        }
1349    }
1350
1351    /**
1352     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
1353     * the given routing path. CEC devices use routing path for its physical address to
1354     * describe the hierarchy of the devices in the network.
1355     *
1356     * @param path routing path or physical address
1357     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
1358     */
1359    @ServiceThreadOnly
1360    final HdmiDeviceInfo getDeviceInfoByPath(int path) {
1361        assertRunOnServiceThread();
1362        for (HdmiDeviceInfo info : getDeviceInfoList(false)) {
1363            if (info.getPhysicalAddress() == path) {
1364                return info;
1365            }
1366        }
1367        return null;
1368    }
1369
1370    /**
1371     * Whether a device of the specified physical address and logical address exists
1372     * in a device info list. However, both are minimal condition and it could
1373     * be different device from the original one.
1374     *
1375     * @param logicalAddress logical address of a device to be searched
1376     * @param physicalAddress physical address of a device to be searched
1377     * @return true if exist; otherwise false
1378     */
1379    @ServiceThreadOnly
1380    private boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1381        assertRunOnServiceThread();
1382        HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress);
1383        if (device == null) {
1384            return false;
1385        }
1386        return device.getPhysicalAddress() == physicalAddress;
1387    }
1388
1389    @Override
1390    @ServiceThreadOnly
1391    void onHotplug(int portId, boolean connected) {
1392        assertRunOnServiceThread();
1393
1394        if (!connected) {
1395            removeCecSwitches(portId);
1396        }
1397        // Tv device will have permanent HotplugDetectionAction.
1398        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1399        if (!hotplugActions.isEmpty()) {
1400            // Note that hotplug action is single action running on a machine.
1401            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1402            // It covers seq #40, #43.
1403            hotplugActions.get(0).pollAllDevicesNow();
1404        }
1405    }
1406
1407    private void removeCecSwitches(int portId) {
1408        Iterator<Integer> it = mCecSwitches.iterator();
1409        while (!it.hasNext()) {
1410            int path = it.next();
1411            if (pathToPortId(path) == portId) {
1412                it.remove();
1413            }
1414        }
1415    }
1416
1417    @ServiceThreadOnly
1418    void setAutoDeviceOff(boolean enabled) {
1419        assertRunOnServiceThread();
1420        mAutoDeviceOff = enabled;
1421    }
1422
1423    @ServiceThreadOnly
1424    void setAutoWakeup(boolean enabled) {
1425        assertRunOnServiceThread();
1426        mAutoWakeup = enabled;
1427    }
1428
1429    @ServiceThreadOnly
1430    boolean getAutoWakeup() {
1431        assertRunOnServiceThread();
1432        return mAutoWakeup;
1433    }
1434
1435    @Override
1436    @ServiceThreadOnly
1437    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1438        super.disableDevice(initiatedByCec, callback);
1439        assertRunOnServiceThread();
1440        // Remove any repeated working actions.
1441        // HotplugDetectionAction will be reinstated during the wake up process.
1442        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1443        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1444        removeAction(DeviceDiscoveryAction.class);
1445        removeAction(HotplugDetectionAction.class);
1446        removeAction(PowerStatusMonitorAction.class);
1447        // Remove recording actions.
1448        removeAction(OneTouchRecordAction.class);
1449        removeAction(TimerRecordingAction.class);
1450
1451        disableSystemAudioIfExist();
1452        disableArcIfExist();
1453        clearDeviceInfoList();
1454        checkIfPendingActionsCleared();
1455    }
1456
1457    @ServiceThreadOnly
1458    private void disableSystemAudioIfExist() {
1459        assertRunOnServiceThread();
1460        if (getAvrDeviceInfo() == null) {
1461            return;
1462        }
1463
1464        // Seq #31.
1465        removeAction(SystemAudioActionFromAvr.class);
1466        removeAction(SystemAudioActionFromTv.class);
1467        removeAction(SystemAudioAutoInitiationAction.class);
1468        removeAction(SystemAudioStatusAction.class);
1469        removeAction(VolumeControlAction.class);
1470
1471        // Turn off the mode but do not write it the settings, so that the next time TV powers on
1472        // the system audio mode setting can be restored automatically.
1473        setSystemAudioMode(false, false);
1474    }
1475
1476    @ServiceThreadOnly
1477    private void disableArcIfExist() {
1478        assertRunOnServiceThread();
1479        HdmiDeviceInfo avr = getAvrDeviceInfo();
1480        if (avr == null) {
1481            return;
1482        }
1483
1484        // Seq #44.
1485        removeAction(RequestArcInitiationAction.class);
1486        if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1487            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1488        }
1489    }
1490
1491    @Override
1492    @ServiceThreadOnly
1493    protected void onStandby(boolean initiatedByCec) {
1494        assertRunOnServiceThread();
1495        // Seq #11
1496        if (!mService.isControlEnabled()) {
1497            return;
1498        }
1499        if (!initiatedByCec && mAutoDeviceOff) {
1500            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1501                    mAddress, Constants.ADDR_BROADCAST));
1502        }
1503    }
1504
1505    boolean isProhibitMode() {
1506        return mService.isProhibitMode();
1507    }
1508
1509    boolean isPowerStandbyOrTransient() {
1510        return mService.isPowerStandbyOrTransient();
1511    }
1512
1513    @ServiceThreadOnly
1514    void displayOsd(int messageId) {
1515        assertRunOnServiceThread();
1516        mService.displayOsd(messageId);
1517    }
1518
1519    @ServiceThreadOnly
1520    void displayOsd(int messageId, int extra) {
1521        assertRunOnServiceThread();
1522        mService.displayOsd(messageId, extra);
1523    }
1524
1525    // Seq #54 and #55
1526    @ServiceThreadOnly
1527    int startOneTouchRecord(int recorderAddress, byte[] recordSource) {
1528        assertRunOnServiceThread();
1529        if (!mService.isControlEnabled()) {
1530            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1531            announceOneTouchRecordResult(ONE_TOUCH_RECORD_CEC_DISABLED);
1532            return Constants.ABORT_NOT_IN_CORRECT_MODE;
1533        }
1534
1535        if (!checkRecorder(recorderAddress)) {
1536            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1537            announceOneTouchRecordResult(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(ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN);
1544            return Constants.ABORT_UNABLE_TO_DETERMINE;
1545        }
1546
1547        addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource));
1548        Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:"
1549                + Arrays.toString(recordSource));
1550        return Constants.ABORT_NO_ERROR;
1551    }
1552
1553    @ServiceThreadOnly
1554    void stopOneTouchRecord(int recorderAddress) {
1555        assertRunOnServiceThread();
1556        if (!mService.isControlEnabled()) {
1557            Slog.w(TAG, "Can not stop one touch record. CEC control is disabled.");
1558            announceOneTouchRecordResult(ONE_TOUCH_RECORD_CEC_DISABLED);
1559            return;
1560        }
1561
1562        if (!checkRecorder(recorderAddress)) {
1563            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1564            announceOneTouchRecordResult(ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1565            return;
1566        }
1567
1568        // Remove one touch record action so that other one touch record can be started.
1569        removeAction(OneTouchRecordAction.class);
1570        mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress));
1571        Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress);
1572    }
1573
1574    private boolean checkRecorder(int recorderAddress) {
1575        HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress);
1576        return (device != null)
1577                && (HdmiUtils.getTypeFromAddress(recorderAddress)
1578                        == HdmiDeviceInfo.DEVICE_RECORDER);
1579    }
1580
1581    private boolean checkRecordSource(byte[] recordSource) {
1582        return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource);
1583    }
1584
1585    @ServiceThreadOnly
1586    void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1587        assertRunOnServiceThread();
1588        if (!mService.isControlEnabled()) {
1589            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1590            announceTimerRecordingResult(TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED);
1591            return;
1592        }
1593
1594        if (!checkRecorder(recorderAddress)) {
1595            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1596            announceTimerRecordingResult(
1597                    TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION);
1598            return;
1599        }
1600
1601        if (!checkTimerRecordingSource(sourceType, recordSource)) {
1602            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1603            announceTimerRecordingResult(
1604                    TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE);
1605            return;
1606        }
1607
1608        addAndStartAction(
1609                new TimerRecordingAction(this, recorderAddress, sourceType, recordSource));
1610        Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:"
1611                + sourceType + ", RecordSource:" + Arrays.toString(recordSource));
1612    }
1613
1614    private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) {
1615        return (recordSource != null)
1616                && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource);
1617    }
1618
1619    @ServiceThreadOnly
1620    void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1621        assertRunOnServiceThread();
1622        if (!mService.isControlEnabled()) {
1623            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1624            announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_CEC_DISABLE);
1625            return;
1626        }
1627
1628        if (!checkRecorder(recorderAddress)) {
1629            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1630            announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION);
1631            return;
1632        }
1633
1634        if (!checkTimerRecordingSource(sourceType, recordSource)) {
1635            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1636            announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1637            return;
1638        }
1639
1640        sendClearTimerMessage(recorderAddress, sourceType, recordSource);
1641    }
1642
1643    private void sendClearTimerMessage(int recorderAddress, int sourceType, byte[] recordSource) {
1644        HdmiCecMessage message = null;
1645        switch (sourceType) {
1646            case TIMER_RECORDING_TYPE_DIGITAL:
1647                message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress,
1648                        recordSource);
1649                break;
1650            case TIMER_RECORDING_TYPE_ANALOGUE:
1651                message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress,
1652                        recordSource);
1653                break;
1654            case TIMER_RECORDING_TYPE_EXTERNAL:
1655                message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress,
1656                        recordSource);
1657                break;
1658            default:
1659                Slog.w(TAG, "Invalid source type:" + recorderAddress);
1660                announceClearTimerRecordingResult(CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1661                return;
1662
1663        }
1664        mService.sendCecCommand(message, new SendMessageCallback() {
1665            @Override
1666            public void onSendCompleted(int error) {
1667                if (error != Constants.SEND_RESULT_SUCCESS) {
1668                    announceClearTimerRecordingResult(
1669                            CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1670                }
1671            }
1672        });
1673    }
1674
1675    void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
1676        HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress);
1677        if (info == null) {
1678            Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress);
1679            return;
1680        }
1681
1682        if (info.getDevicePowerStatus() == newPowerStatus) {
1683            return;
1684        }
1685
1686        HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus);
1687        // addDeviceInfo replaces old device info with new one if exists.
1688        addDeviceInfo(newInfo);
1689
1690        invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE);
1691    }
1692
1693    @Override
1694    protected boolean handleMenuStatus(HdmiCecMessage message) {
1695        // Do nothing and just return true not to prevent from responding <Feature Abort>.
1696        return true;
1697    }
1698
1699    @Override
1700    protected void sendStandby(int deviceId) {
1701        HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId);
1702        if (targetDevice == null) {
1703            return;
1704        }
1705        int targetAddress = targetDevice.getLogicalAddress();
1706        mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
1707    }
1708
1709    @Override
1710    protected void dump(final IndentingPrintWriter pw) {
1711        super.dump(pw);
1712        pw.println("mArcEstablished: " + mArcEstablished);
1713        pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled);
1714        pw.println("mSystemAudioActivated: " + mSystemAudioActivated);
1715        pw.println("mSystemAudioMute: " + mSystemAudioMute);
1716        pw.println("mAutoDeviceOff: " + mAutoDeviceOff);
1717        pw.println("mAutoWakeup: " + mAutoWakeup);
1718        pw.println("mSkipRoutingControl: " + mSkipRoutingControl);
1719        pw.println("CEC devices:");
1720        pw.increaseIndent();
1721        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1722            pw.println(info);
1723        }
1724        pw.decreaseIndent();
1725    }
1726}
1727