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