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