HdmiCecLocalDeviceTv.java revision 326aef0c9402742e29c4503c857f93e75cf9a6ec
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                        message.getSource(),
1033                        HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS);
1034            }
1035            return super.handleRecordTvScreen(message);
1036        }
1037
1038        int recorderAddress = message.getSource();
1039        byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress);
1040        int reason = startOneTouchRecord(recorderAddress, recordSource);
1041        if (reason != Constants.ABORT_NO_ERROR) {
1042            mService.maySendFeatureAbortCommand(message, reason);
1043        }
1044        return true;
1045    }
1046
1047    @Override
1048    protected boolean handleTimerClearedStatus(HdmiCecMessage message) {
1049        byte[] params = message.getParams();
1050        int timerClearedStatusData = params[0] & 0xFF;
1051        announceTimerRecordingResult(message.getSource(), timerClearedStatusData);
1052        return true;
1053    }
1054
1055    void announceOneTouchRecordResult(int recorderAddress, int result) {
1056        mService.invokeOneTouchRecordResult(recorderAddress, result);
1057    }
1058
1059    void announceTimerRecordingResult(int recorderAddress, int result) {
1060        mService.invokeTimerRecordingResult(recorderAddress, result);
1061    }
1062
1063    void announceClearTimerRecordingResult(int recorderAddress, int result) {
1064        mService.invokeClearTimerRecordingResult(recorderAddress, result);
1065    }
1066
1067    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
1068        return mService.isControlEnabled()
1069                && message.getSource() == Constants.ADDR_AUDIO_SYSTEM
1070                && (message.getDestination() == Constants.ADDR_TV
1071                        || message.getDestination() == Constants.ADDR_BROADCAST)
1072                && getAvrDeviceInfo() != null;
1073    }
1074
1075    /**
1076     * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
1077     * logical address as new device info's.
1078     *
1079     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1080     *
1081     * @param deviceInfo a new {@link HdmiDeviceInfo} to be added.
1082     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo}
1083     *         that has the same logical address as new one has.
1084     */
1085    @ServiceThreadOnly
1086    private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
1087        assertRunOnServiceThread();
1088        HdmiDeviceInfo oldDeviceInfo = getCecDeviceInfo(deviceInfo.getLogicalAddress());
1089        if (oldDeviceInfo != null) {
1090            removeDeviceInfo(deviceInfo.getId());
1091        }
1092        mDeviceInfos.append(deviceInfo.getId(), deviceInfo);
1093        updateSafeDeviceInfoList();
1094        return oldDeviceInfo;
1095    }
1096
1097    /**
1098     * Remove a device info corresponding to the given {@code logicalAddress}.
1099     * It returns removed {@link HdmiDeviceInfo} if exists.
1100     *
1101     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1102     *
1103     * @param id id of device to be removed
1104     * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null}
1105     */
1106    @ServiceThreadOnly
1107    private HdmiDeviceInfo removeDeviceInfo(int id) {
1108        assertRunOnServiceThread();
1109        HdmiDeviceInfo deviceInfo = mDeviceInfos.get(id);
1110        if (deviceInfo != null) {
1111            mDeviceInfos.remove(id);
1112        }
1113        updateSafeDeviceInfoList();
1114        return deviceInfo;
1115    }
1116
1117    /**
1118     * Return a list of all {@link HdmiDeviceInfo}.
1119     *
1120     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1121     * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputsLocked} which
1122     * does not include local device.
1123     */
1124    @ServiceThreadOnly
1125    List<HdmiDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
1126        assertRunOnServiceThread();
1127        if (includeLocalDevice) {
1128            return HdmiUtils.sparseArrayToList(mDeviceInfos);
1129        } else {
1130            ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1131            for (int i = 0; i < mDeviceInfos.size(); ++i) {
1132                HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1133                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
1134                    infoList.add(info);
1135                }
1136            }
1137            return infoList;
1138        }
1139    }
1140
1141    /**
1142     * Return external input devices.
1143     */
1144    List<HdmiDeviceInfo> getSafeExternalInputsLocked() {
1145        return mSafeExternalInputs;
1146    }
1147
1148    @ServiceThreadOnly
1149    private void updateSafeDeviceInfoList() {
1150        assertRunOnServiceThread();
1151        List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
1152        List<HdmiDeviceInfo> externalInputs = getInputDevices();
1153        synchronized (mLock) {
1154            mSafeAllDeviceInfos = copiedDevices;
1155            mSafeExternalInputs = externalInputs;
1156        }
1157    }
1158
1159    /**
1160     * Return a list of external cec input (source) devices.
1161     *
1162     * <p>Note that this effectively excludes non-source devices like system audio,
1163     * secondary TV.
1164     */
1165    private List<HdmiDeviceInfo> getInputDevices() {
1166        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1167        for (int i = 0; i < mDeviceInfos.size(); ++i) {
1168            HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1169            if (isLocalDeviceAddress(info.getLogicalAddress())) {
1170                continue;
1171            }
1172            if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1173                infoList.add(info);
1174            }
1175        }
1176        return infoList;
1177    }
1178
1179    // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch.
1180    // Returns true if the policy is set to true, and the device to check does not have
1181    // a parent CEC device (which should be the CEC-enabled switch) in the list.
1182    private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) {
1183        return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH
1184                && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches);
1185    }
1186
1187    private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) {
1188        for (int switchPath : switches) {
1189            if (isParentPath(switchPath, path)) {
1190                return true;
1191            }
1192        }
1193        return false;
1194    }
1195
1196    private static boolean isParentPath(int parentPath, int childPath) {
1197        // (A000, AB00) (AB00, ABC0), (ABC0, ABCD)
1198        // If child's last non-zero nibble is removed, the result equals to the parent.
1199        for (int i = 0; i <= 12; i += 4) {
1200            int nibble = (childPath >> i) & 0xF;
1201            if (nibble != 0) {
1202                int parentNibble = (parentPath >> i) & 0xF;
1203                return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4);
1204            }
1205        }
1206        return false;
1207    }
1208
1209    private void invokeDeviceEventListener(HdmiDeviceInfo info, int status) {
1210        if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1211            mService.invokeDeviceEventListeners(info, status);
1212        }
1213    }
1214
1215    private boolean isLocalDeviceAddress(int address) {
1216        return mLocalDeviceAddresses.contains(address);
1217    }
1218
1219    @ServiceThreadOnly
1220    HdmiDeviceInfo getAvrDeviceInfo() {
1221        assertRunOnServiceThread();
1222        return getCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1223    }
1224
1225    /**
1226     * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}.
1227     *
1228     * This is not thread-safe. For thread safety, call {@link #getSafeCecDeviceInfo(int)}.
1229     *
1230     * @param logicalAddress logical address of the device to be retrieved
1231     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1232     *         Returns null if no logical address matched
1233     */
1234    @ServiceThreadOnly
1235    HdmiDeviceInfo getCecDeviceInfo(int logicalAddress) {
1236        assertRunOnServiceThread();
1237        return mDeviceInfos.get(HdmiDeviceInfo.idForCecDevice(logicalAddress));
1238    }
1239
1240    boolean hasSystemAudioDevice() {
1241        return getSafeAvrDeviceInfo() != null;
1242    }
1243
1244    HdmiDeviceInfo getSafeAvrDeviceInfo() {
1245        return getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1246    }
1247
1248    /**
1249     * Thread safe version of {@link #getCecDeviceInfo(int)}.
1250     *
1251     * @param logicalAddress logical address to be retrieved
1252     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1253     *         Returns null if no logical address matched
1254     */
1255    HdmiDeviceInfo getSafeCecDeviceInfo(int logicalAddress) {
1256        synchronized (mLock) {
1257            for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1258                if (info.isCecDevice() && info.getLogicalAddress() == logicalAddress) {
1259                    return info;
1260                }
1261            }
1262            return null;
1263        }
1264    }
1265
1266    List<HdmiDeviceInfo> getSafeCecDevicesLocked() {
1267        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1268        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1269            if (isLocalDeviceAddress(info.getLogicalAddress())) {
1270                continue;
1271            }
1272            infoList.add(info);
1273        }
1274        return infoList;
1275    }
1276
1277    /**
1278     * Called when a device is newly added or a new device is detected or
1279     * existing device is updated.
1280     *
1281     * @param info device info of a new device.
1282     */
1283    @ServiceThreadOnly
1284    final void addCecDevice(HdmiDeviceInfo info) {
1285        assertRunOnServiceThread();
1286        addDeviceInfo(info);
1287        if (info.getLogicalAddress() == mAddress) {
1288            // The addition of TV device itself should not be notified.
1289            return;
1290        }
1291        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
1292    }
1293
1294    /**
1295     * Called when a device is removed or removal of device is detected.
1296     *
1297     * @param address a logical address of a device to be removed
1298     */
1299    @ServiceThreadOnly
1300    final void removeCecDevice(int address) {
1301        assertRunOnServiceThread();
1302        HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
1303
1304        mCecMessageCache.flushMessagesFrom(address);
1305        invokeDeviceEventListener(info, HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE);
1306    }
1307
1308    @ServiceThreadOnly
1309    void handleRemoveActiveRoutingPath(int path) {
1310        assertRunOnServiceThread();
1311        // Seq #23
1312        if (isTailOfActivePath(path, getActivePath())) {
1313            removeAction(RoutingControlAction.class);
1314            int newPath = mService.portIdToPath(getActivePortId());
1315            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
1316                    mAddress, getActivePath(), newPath));
1317            mActiveSource.invalidate();
1318            addAndStartAction(new RoutingControlAction(this, getActivePath(), true, null));
1319        }
1320    }
1321
1322    /**
1323     * Launch routing control process.
1324     *
1325     * @param routingForBootup true if routing control is initiated due to One Touch Play
1326     *        or TV power on
1327     */
1328    @ServiceThreadOnly
1329    void launchRoutingControl(boolean routingForBootup) {
1330        assertRunOnServiceThread();
1331        // Seq #24
1332        if (getActivePortId() != Constants.INVALID_PORT_ID) {
1333            if (!routingForBootup && !isProhibitMode()) {
1334                removeAction(RoutingControlAction.class);
1335                int newPath = mService.portIdToPath(getActivePortId());
1336                setActivePath(newPath);
1337                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
1338                        getActivePath(), newPath));
1339                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1340                        routingForBootup, null));
1341            }
1342        } else {
1343            int activePath = mService.getPhysicalAddress();
1344            setActivePath(activePath);
1345            if (!routingForBootup) {
1346                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1347                        activePath));
1348            }
1349        }
1350    }
1351
1352    /**
1353     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
1354     * the given routing path. CEC devices use routing path for its physical address to
1355     * describe the hierarchy of the devices in the network.
1356     *
1357     * @param path routing path or physical address
1358     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
1359     */
1360    @ServiceThreadOnly
1361    final HdmiDeviceInfo getDeviceInfoByPath(int path) {
1362        assertRunOnServiceThread();
1363        for (HdmiDeviceInfo info : getDeviceInfoList(false)) {
1364            if (info.getPhysicalAddress() == path) {
1365                return info;
1366            }
1367        }
1368        return null;
1369    }
1370
1371    /**
1372     * Whether a device of the specified physical address and logical address exists
1373     * in a device info list. However, both are minimal condition and it could
1374     * be different device from the original one.
1375     *
1376     * @param logicalAddress logical address of a device to be searched
1377     * @param physicalAddress physical address of a device to be searched
1378     * @return true if exist; otherwise false
1379     */
1380    @ServiceThreadOnly
1381    private boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1382        assertRunOnServiceThread();
1383        HdmiDeviceInfo device = getCecDeviceInfo(logicalAddress);
1384        if (device == null) {
1385            return false;
1386        }
1387        return device.getPhysicalAddress() == physicalAddress;
1388    }
1389
1390    @Override
1391    @ServiceThreadOnly
1392    void onHotplug(int portId, boolean connected) {
1393        assertRunOnServiceThread();
1394
1395        if (!connected) {
1396            removeCecSwitches(portId);
1397        }
1398        // Tv device will have permanent HotplugDetectionAction.
1399        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1400        if (!hotplugActions.isEmpty()) {
1401            // Note that hotplug action is single action running on a machine.
1402            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1403            // It covers seq #40, #43.
1404            hotplugActions.get(0).pollAllDevicesNow();
1405        }
1406    }
1407
1408    private void removeCecSwitches(int portId) {
1409        Iterator<Integer> it = mCecSwitches.iterator();
1410        while (!it.hasNext()) {
1411            int path = it.next();
1412            if (pathToPortId(path) == portId) {
1413                it.remove();
1414            }
1415        }
1416    }
1417
1418    @ServiceThreadOnly
1419    void setAutoDeviceOff(boolean enabled) {
1420        assertRunOnServiceThread();
1421        mAutoDeviceOff = enabled;
1422    }
1423
1424    @ServiceThreadOnly
1425    void setAutoWakeup(boolean enabled) {
1426        assertRunOnServiceThread();
1427        mAutoWakeup = enabled;
1428    }
1429
1430    @ServiceThreadOnly
1431    boolean getAutoWakeup() {
1432        assertRunOnServiceThread();
1433        return mAutoWakeup;
1434    }
1435
1436    @Override
1437    @ServiceThreadOnly
1438    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1439        super.disableDevice(initiatedByCec, callback);
1440        assertRunOnServiceThread();
1441        // Remove any repeated working actions.
1442        // HotplugDetectionAction will be reinstated during the wake up process.
1443        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1444        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1445        removeAction(DeviceDiscoveryAction.class);
1446        removeAction(HotplugDetectionAction.class);
1447        removeAction(PowerStatusMonitorAction.class);
1448        // Remove recording actions.
1449        removeAction(OneTouchRecordAction.class);
1450        removeAction(TimerRecordingAction.class);
1451
1452        disableSystemAudioIfExist();
1453        disableArcIfExist();
1454        clearDeviceInfoList();
1455        checkIfPendingActionsCleared();
1456    }
1457
1458    @ServiceThreadOnly
1459    private void disableSystemAudioIfExist() {
1460        assertRunOnServiceThread();
1461        if (getAvrDeviceInfo() == null) {
1462            return;
1463        }
1464
1465        // Seq #31.
1466        removeAction(SystemAudioActionFromAvr.class);
1467        removeAction(SystemAudioActionFromTv.class);
1468        removeAction(SystemAudioAutoInitiationAction.class);
1469        removeAction(SystemAudioStatusAction.class);
1470        removeAction(VolumeControlAction.class);
1471
1472        // Turn off the mode but do not write it the settings, so that the next time TV powers on
1473        // the system audio mode setting can be restored automatically.
1474        setSystemAudioMode(false, false);
1475    }
1476
1477    @ServiceThreadOnly
1478    private void disableArcIfExist() {
1479        assertRunOnServiceThread();
1480        HdmiDeviceInfo avr = getAvrDeviceInfo();
1481        if (avr == null) {
1482            return;
1483        }
1484
1485        // Seq #44.
1486        removeAction(RequestArcInitiationAction.class);
1487        if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1488            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1489        }
1490    }
1491
1492    @Override
1493    @ServiceThreadOnly
1494    protected void onStandby(boolean initiatedByCec) {
1495        assertRunOnServiceThread();
1496        // Seq #11
1497        if (!mService.isControlEnabled()) {
1498            return;
1499        }
1500        if (!initiatedByCec && mAutoDeviceOff) {
1501            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1502                    mAddress, Constants.ADDR_BROADCAST));
1503        }
1504    }
1505
1506    boolean isProhibitMode() {
1507        return mService.isProhibitMode();
1508    }
1509
1510    boolean isPowerStandbyOrTransient() {
1511        return mService.isPowerStandbyOrTransient();
1512    }
1513
1514    @ServiceThreadOnly
1515    void displayOsd(int messageId) {
1516        assertRunOnServiceThread();
1517        mService.displayOsd(messageId);
1518    }
1519
1520    @ServiceThreadOnly
1521    void displayOsd(int messageId, int extra) {
1522        assertRunOnServiceThread();
1523        mService.displayOsd(messageId, extra);
1524    }
1525
1526    // Seq #54 and #55
1527    @ServiceThreadOnly
1528    int startOneTouchRecord(int recorderAddress, byte[] recordSource) {
1529        assertRunOnServiceThread();
1530        if (!mService.isControlEnabled()) {
1531            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1532            announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED);
1533            return Constants.ABORT_NOT_IN_CORRECT_MODE;
1534        }
1535
1536        if (!checkRecorder(recorderAddress)) {
1537            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1538            announceOneTouchRecordResult(recorderAddress,
1539                    ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1540            return Constants.ABORT_NOT_IN_CORRECT_MODE;
1541        }
1542
1543        if (!checkRecordSource(recordSource)) {
1544            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1545            announceOneTouchRecordResult(recorderAddress,
1546                    ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN);
1547            return Constants.ABORT_UNABLE_TO_DETERMINE;
1548        }
1549
1550        addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource));
1551        Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:"
1552                + Arrays.toString(recordSource));
1553        return Constants.ABORT_NO_ERROR;
1554    }
1555
1556    @ServiceThreadOnly
1557    void stopOneTouchRecord(int recorderAddress) {
1558        assertRunOnServiceThread();
1559        if (!mService.isControlEnabled()) {
1560            Slog.w(TAG, "Can not stop one touch record. CEC control is disabled.");
1561            announceOneTouchRecordResult(recorderAddress, ONE_TOUCH_RECORD_CEC_DISABLED);
1562            return;
1563        }
1564
1565        if (!checkRecorder(recorderAddress)) {
1566            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1567            announceOneTouchRecordResult(recorderAddress,
1568                    ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION);
1569            return;
1570        }
1571
1572        // Remove one touch record action so that other one touch record can be started.
1573        removeAction(OneTouchRecordAction.class);
1574        mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress));
1575        Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress);
1576    }
1577
1578    private boolean checkRecorder(int recorderAddress) {
1579        HdmiDeviceInfo device = getCecDeviceInfo(recorderAddress);
1580        return (device != null)
1581                && (HdmiUtils.getTypeFromAddress(recorderAddress)
1582                        == HdmiDeviceInfo.DEVICE_RECORDER);
1583    }
1584
1585    private boolean checkRecordSource(byte[] recordSource) {
1586        return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource);
1587    }
1588
1589    @ServiceThreadOnly
1590    void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1591        assertRunOnServiceThread();
1592        if (!mService.isControlEnabled()) {
1593            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1594            announceTimerRecordingResult(recorderAddress,
1595                    TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED);
1596            return;
1597        }
1598
1599        if (!checkRecorder(recorderAddress)) {
1600            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1601            announceTimerRecordingResult(recorderAddress,
1602                    TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION);
1603            return;
1604        }
1605
1606        if (!checkTimerRecordingSource(sourceType, recordSource)) {
1607            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1608            announceTimerRecordingResult(
1609                    recorderAddress,
1610                    TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE);
1611            return;
1612        }
1613
1614        addAndStartAction(
1615                new TimerRecordingAction(this, recorderAddress, sourceType, recordSource));
1616        Slog.i(TAG, "Start [Timer Recording]-Target:" + recorderAddress + ", SourceType:"
1617                + sourceType + ", RecordSource:" + Arrays.toString(recordSource));
1618    }
1619
1620    private boolean checkTimerRecordingSource(int sourceType, byte[] recordSource) {
1621        return (recordSource != null)
1622                && HdmiTimerRecordSources.checkTimerRecordSource(sourceType, recordSource);
1623    }
1624
1625    @ServiceThreadOnly
1626    void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
1627        assertRunOnServiceThread();
1628        if (!mService.isControlEnabled()) {
1629            Slog.w(TAG, "Can not start one touch record. CEC control is disabled.");
1630            announceClearTimerRecordingResult(recorderAddress, CLEAR_TIMER_STATUS_CEC_DISABLE);
1631            return;
1632        }
1633
1634        if (!checkRecorder(recorderAddress)) {
1635            Slog.w(TAG, "Invalid recorder address:" + recorderAddress);
1636            announceClearTimerRecordingResult(recorderAddress,
1637                    CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION);
1638            return;
1639        }
1640
1641        if (!checkTimerRecordingSource(sourceType, recordSource)) {
1642            Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource));
1643            announceClearTimerRecordingResult(recorderAddress,
1644                    CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1645            return;
1646        }
1647
1648        sendClearTimerMessage(recorderAddress, sourceType, recordSource);
1649    }
1650
1651    private void sendClearTimerMessage(final int recorderAddress, int sourceType,
1652            byte[] recordSource) {
1653        HdmiCecMessage message = null;
1654        switch (sourceType) {
1655            case TIMER_RECORDING_TYPE_DIGITAL:
1656                message = HdmiCecMessageBuilder.buildClearDigitalTimer(mAddress, recorderAddress,
1657                        recordSource);
1658                break;
1659            case TIMER_RECORDING_TYPE_ANALOGUE:
1660                message = HdmiCecMessageBuilder.buildClearAnalogueTimer(mAddress, recorderAddress,
1661                        recordSource);
1662                break;
1663            case TIMER_RECORDING_TYPE_EXTERNAL:
1664                message = HdmiCecMessageBuilder.buildClearExternalTimer(mAddress, recorderAddress,
1665                        recordSource);
1666                break;
1667            default:
1668                Slog.w(TAG, "Invalid source type:" + recorderAddress);
1669                announceClearTimerRecordingResult(recorderAddress,
1670                        CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1671                return;
1672
1673        }
1674        mService.sendCecCommand(message, new SendMessageCallback() {
1675            @Override
1676            public void onSendCompleted(int error) {
1677                if (error != Constants.SEND_RESULT_SUCCESS) {
1678                    announceClearTimerRecordingResult(recorderAddress,
1679                            CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE);
1680                }
1681            }
1682        });
1683    }
1684
1685    void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
1686        HdmiDeviceInfo info = getCecDeviceInfo(logicalAddress);
1687        if (info == null) {
1688            Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress);
1689            return;
1690        }
1691
1692        if (info.getDevicePowerStatus() == newPowerStatus) {
1693            return;
1694        }
1695
1696        HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus);
1697        // addDeviceInfo replaces old device info with new one if exists.
1698        addDeviceInfo(newInfo);
1699
1700        invokeDeviceEventListener(newInfo, HdmiControlManager.DEVICE_EVENT_UPDATE_DEVICE);
1701    }
1702
1703    @Override
1704    protected boolean handleMenuStatus(HdmiCecMessage message) {
1705        // Do nothing and just return true not to prevent from responding <Feature Abort>.
1706        return true;
1707    }
1708
1709    @Override
1710    protected void sendStandby(int deviceId) {
1711        HdmiDeviceInfo targetDevice = mDeviceInfos.get(deviceId);
1712        if (targetDevice == null) {
1713            return;
1714        }
1715        int targetAddress = targetDevice.getLogicalAddress();
1716        mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(mAddress, targetAddress));
1717    }
1718
1719    @Override
1720    protected void dump(final IndentingPrintWriter pw) {
1721        super.dump(pw);
1722        pw.println("mArcEstablished: " + mArcEstablished);
1723        pw.println("mArcFeatureEnabled: " + mArcFeatureEnabled);
1724        pw.println("mSystemAudioActivated: " + mSystemAudioActivated);
1725        pw.println("mSystemAudioMute: " + mSystemAudioMute);
1726        pw.println("mAutoDeviceOff: " + mAutoDeviceOff);
1727        pw.println("mAutoWakeup: " + mAutoWakeup);
1728        pw.println("mSkipRoutingControl: " + mSkipRoutingControl);
1729        pw.println("CEC devices:");
1730        pw.increaseIndent();
1731        for (HdmiDeviceInfo info : mSafeAllDeviceInfos) {
1732            pw.println(info);
1733        }
1734        pw.decreaseIndent();
1735    }
1736}
1737