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