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