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