HdmiCecLocalDeviceTv.java revision 5008486b09c588bf3409b70d9ee29225e8593c64
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.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.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                        addAndStartAction(new PowerStatusMonitorAction(HdmiCecLocalDeviceTv.this));
621
622                        // If there is AVR, initiate System Audio Auto initiation action,
623                        // which turns on and off system audio according to last system
624                        // audio setting.
625                        if (mSystemAudioActivated && getAvrDeviceInfo() != null) {
626                            addAndStartAction(new SystemAudioAutoInitiationAction(
627                                    HdmiCecLocalDeviceTv.this,
628                                    getAvrDeviceInfo().getLogicalAddress()));
629                            if (mArcEstablished) {
630                                startArcAction(true);
631                            }
632                        }
633                    }
634                });
635        addAndStartAction(action);
636    }
637
638    // Clear all device info.
639    @ServiceThreadOnly
640    private void clearDeviceInfoList() {
641        assertRunOnServiceThread();
642        for (HdmiDeviceInfo info : mSafeExternalInputs) {
643            invokeDeviceEventListener(info, false);
644        }
645        mDeviceInfos.clear();
646        updateSafeDeviceInfoList();
647    }
648
649    @ServiceThreadOnly
650    // Seq #32
651    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
652        assertRunOnServiceThread();
653        if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
654            setSystemAudioMode(false, true);
655            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
656            return;
657        }
658        HdmiDeviceInfo avr = getAvrDeviceInfo();
659        if (avr == null) {
660            setSystemAudioMode(false, true);
661            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
662            return;
663        }
664
665        addAndStartAction(
666                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
667    }
668
669    // # Seq 25
670    void setSystemAudioMode(boolean on, boolean updateSetting) {
671        if (updateSetting) {
672            mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
673        }
674        updateAudioManagerForSystemAudio(on);
675        synchronized (mLock) {
676            if (mSystemAudioActivated != on) {
677                mSystemAudioActivated = on;
678                mService.announceSystemAudioModeChange(on);
679            }
680        }
681    }
682
683    private void updateAudioManagerForSystemAudio(boolean on) {
684        // TODO: remove output device, once update AudioManager api.
685        mService.getAudioManager().setHdmiSystemAudioSupported(on);
686    }
687
688    boolean isSystemAudioActivated() {
689        if (getAvrDeviceInfo() == null) {
690            return false;
691        }
692        synchronized (mLock) {
693            return mSystemAudioActivated;
694        }
695    }
696
697    boolean getSystemAudioModeSetting() {
698        return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false);
699    }
700
701    /**
702     * Change ARC status into the given {@code enabled} status.
703     *
704     * @return {@code true} if ARC was in "Enabled" status
705     */
706    @ServiceThreadOnly
707    boolean setArcStatus(boolean enabled) {
708        assertRunOnServiceThread();
709        boolean oldStatus = mArcEstablished;
710        // 1. Enable/disable ARC circuit.
711        mService.setAudioReturnChannel(enabled);
712        // 2. Notify arc status to audio service.
713        notifyArcStatusToAudioService(enabled);
714        // 3. Update arc status;
715        mArcEstablished = enabled;
716        return oldStatus;
717    }
718
719    private void notifyArcStatusToAudioService(boolean enabled) {
720        // Note that we don't set any name to ARC.
721        mService.getAudioManager().setWiredDeviceConnectionState(
722                AudioSystem.DEVICE_OUT_HDMI_ARC,
723                enabled ? 1 : 0, "");
724    }
725
726    /**
727     * Returns whether ARC is enabled or not.
728     */
729    @ServiceThreadOnly
730    boolean isArcEstabilished() {
731        assertRunOnServiceThread();
732        return mArcFeatureEnabled && mArcEstablished;
733    }
734
735    @ServiceThreadOnly
736    void changeArcFeatureEnabled(boolean enabled) {
737        assertRunOnServiceThread();
738
739        if (mArcFeatureEnabled != enabled) {
740            if (enabled) {
741                if (!mArcEstablished) {
742                    startArcAction(true);
743                }
744            } else {
745                if (mArcEstablished) {
746                    startArcAction(false);
747                }
748            }
749            mArcFeatureEnabled = enabled;
750        }
751    }
752
753    @ServiceThreadOnly
754    boolean isArcFeatureEnabled() {
755        assertRunOnServiceThread();
756        return mArcFeatureEnabled;
757    }
758
759    @ServiceThreadOnly
760    private void startArcAction(boolean enabled) {
761        assertRunOnServiceThread();
762        HdmiDeviceInfo info = getAvrDeviceInfo();
763        if (info == null) {
764            return;
765        }
766        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
767            return;
768        }
769
770        // Terminate opposite action and start action if not exist.
771        if (enabled) {
772            removeAction(RequestArcTerminationAction.class);
773            if (!hasAction(RequestArcInitiationAction.class)) {
774                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
775            }
776        } else {
777            removeAction(RequestArcInitiationAction.class);
778            if (!hasAction(RequestArcTerminationAction.class)) {
779                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
780            }
781        }
782    }
783
784    void setAudioStatus(boolean mute, int volume) {
785        synchronized (mLock) {
786            mSystemAudioMute = mute;
787            mSystemAudioVolume = volume;
788            int maxVolume = mService.getAudioManager().getStreamMaxVolume(
789                    AudioManager.STREAM_MUSIC);
790            mService.setAudioStatus(mute,
791                    VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
792        }
793    }
794
795    @ServiceThreadOnly
796    void changeVolume(int curVolume, int delta, int maxVolume) {
797        assertRunOnServiceThread();
798        if (delta == 0 || !isSystemAudioActivated()) {
799            return;
800        }
801
802        int targetVolume = curVolume + delta;
803        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
804        synchronized (mLock) {
805            // If new volume is the same as current system audio volume, just ignore it.
806            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
807            if (cecVolume == mSystemAudioVolume) {
808                // Update tv volume with system volume value.
809                mService.setAudioStatus(false,
810                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
811                return;
812            }
813        }
814
815        // Remove existing volume action.
816        removeAction(VolumeControlAction.class);
817
818        HdmiDeviceInfo avr = getAvrDeviceInfo();
819        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
820                cecVolume, delta > 0));
821    }
822
823    @ServiceThreadOnly
824    void changeMute(boolean mute) {
825        assertRunOnServiceThread();
826        if (!isSystemAudioActivated()) {
827            return;
828        }
829
830        // Remove existing volume action.
831        removeAction(VolumeControlAction.class);
832        HdmiDeviceInfo avr = getAvrDeviceInfo();
833        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
834    }
835
836    @Override
837    @ServiceThreadOnly
838    protected boolean handleInitiateArc(HdmiCecMessage message) {
839        assertRunOnServiceThread();
840        // In case where <Initiate Arc> is started by <Request ARC Initiation>
841        // need to clean up RequestArcInitiationAction.
842        removeAction(RequestArcInitiationAction.class);
843        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
844                message.getSource(), true);
845        addAndStartAction(action);
846        return true;
847    }
848
849    @Override
850    @ServiceThreadOnly
851    protected boolean handleTerminateArc(HdmiCecMessage message) {
852        assertRunOnServiceThread();
853        // In case where <Terminate Arc> is started by <Request ARC Termination>
854        // need to clean up RequestArcInitiationAction.
855        removeAction(RequestArcTerminationAction.class);
856        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
857                message.getSource(), false);
858        addAndStartAction(action);
859        return true;
860    }
861
862    @Override
863    @ServiceThreadOnly
864    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
865        assertRunOnServiceThread();
866        if (!isMessageForSystemAudio(message)) {
867            return false;
868        }
869        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
870                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
871        addAndStartAction(action);
872        return true;
873    }
874
875    @Override
876    @ServiceThreadOnly
877    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
878        assertRunOnServiceThread();
879        if (!isMessageForSystemAudio(message)) {
880            return false;
881        }
882        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
883        return true;
884    }
885
886    // Seq #53
887    @Override
888    @ServiceThreadOnly
889    protected boolean handleRecordTvScreen(HdmiCecMessage message) {
890        List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class);
891        if (!actions.isEmpty()) {
892            // Assumes only one OneTouchRecordAction.
893            OneTouchRecordAction action = actions.get(0);
894            if (action.getRecorderAddress() != message.getSource()) {
895                announceOneTouchRecordResult(
896                        HdmiControlManager.ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS);
897            }
898            return super.handleRecordTvScreen(message);
899        }
900
901        int recorderAddress = message.getSource();
902        byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress);
903        startOneTouchRecord(recorderAddress, recordSource);
904        return true;
905    }
906
907    @Override
908    protected boolean handleTimerClearedStatus(HdmiCecMessage message) {
909        byte[] params = message.getParams();
910        int timerClearedStatusData = params[0] & 0xFF;
911        announceTimerRecordingResult(timerClearedStatusData);
912        return true;
913    }
914
915    void announceOneTouchRecordResult(int result) {
916        mService.invokeOneTouchRecordResult(result);
917    }
918
919    void announceTimerRecordingResult(int result) {
920        mService.invokeTimerRecordingResult(result);
921    }
922
923    void announceClearTimerRecordingResult(int result) {
924        mService.invokeClearTimerRecordingResult(result);
925    }
926
927    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
928        if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM
929                || message.getDestination() != Constants.ADDR_TV
930                || getAvrDeviceInfo() == null) {
931            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
932            return false;
933        }
934        return true;
935    }
936
937    /**
938     * Add a new {@link HdmiDeviceInfo}. It returns old device info which has the same
939     * logical address as new device info's.
940     *
941     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
942     *
943     * @param deviceInfo a new {@link HdmiDeviceInfo} to be added.
944     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiDeviceInfo}
945     *         that has the same logical address as new one has.
946     */
947    @ServiceThreadOnly
948    private HdmiDeviceInfo addDeviceInfo(HdmiDeviceInfo deviceInfo) {
949        assertRunOnServiceThread();
950        HdmiDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
951        if (oldDeviceInfo != null) {
952            removeDeviceInfo(deviceInfo.getLogicalAddress());
953        }
954        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
955        updateSafeDeviceInfoList();
956        return oldDeviceInfo;
957    }
958
959    /**
960     * Remove a device info corresponding to the given {@code logicalAddress}.
961     * It returns removed {@link HdmiDeviceInfo} if exists.
962     *
963     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
964     *
965     * @param logicalAddress logical address of device to be removed
966     * @return removed {@link HdmiDeviceInfo} it exists. Otherwise, returns {@code null}
967     */
968    @ServiceThreadOnly
969    private HdmiDeviceInfo removeDeviceInfo(int logicalAddress) {
970        assertRunOnServiceThread();
971        HdmiDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
972        if (deviceInfo != null) {
973            mDeviceInfos.remove(logicalAddress);
974        }
975        updateSafeDeviceInfoList();
976        return deviceInfo;
977    }
978
979    /**
980     * Return a list of all {@link HdmiDeviceInfo}.
981     *
982     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
983     * This is not thread-safe. For thread safety, call {@link #getSafeExternalInputs} which
984     * does not include local device.
985     */
986    @ServiceThreadOnly
987    List<HdmiDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
988        assertRunOnServiceThread();
989        if (includelLocalDevice) {
990            return HdmiUtils.sparseArrayToList(mDeviceInfos);
991        } else {
992            ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
993            for (int i = 0; i < mDeviceInfos.size(); ++i) {
994                HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
995                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
996                    infoList.add(info);
997                }
998            }
999            return infoList;
1000        }
1001    }
1002
1003    /**
1004     * Return external input devices.
1005     */
1006    List<HdmiDeviceInfo> getSafeExternalInputs() {
1007        synchronized (mLock) {
1008            return mSafeExternalInputs;
1009        }
1010    }
1011
1012    @ServiceThreadOnly
1013    private void updateSafeDeviceInfoList() {
1014        assertRunOnServiceThread();
1015        List<HdmiDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
1016        List<HdmiDeviceInfo> externalInputs = getInputDevices();
1017        synchronized (mLock) {
1018            mSafeAllDeviceInfos = copiedDevices;
1019            mSafeExternalInputs = externalInputs;
1020        }
1021    }
1022
1023    /**
1024     * Return a list of external cec input (source) devices.
1025     *
1026     * <p>Note that this effectively excludes non-source devices like system audio,
1027     * secondary TV.
1028     */
1029    private List<HdmiDeviceInfo> getInputDevices() {
1030        ArrayList<HdmiDeviceInfo> infoList = new ArrayList<>();
1031        for (int i = 0; i < mDeviceInfos.size(); ++i) {
1032            HdmiDeviceInfo info = mDeviceInfos.valueAt(i);
1033            if (isLocalDeviceAddress(i)) {
1034                continue;
1035            }
1036            if (info.isSourceType() && !hideDevicesBehindLegacySwitch(info)) {
1037                infoList.add(info);
1038            }
1039        }
1040        return infoList;
1041    }
1042
1043    // Check if we are hiding CEC devices connected to a legacy (non-CEC) switch.
1044    // Returns true if the policy is set to true, and the device to check does not have
1045    // a parent CEC device (which should be the CEC-enabled switch) in the list.
1046    private boolean hideDevicesBehindLegacySwitch(HdmiDeviceInfo info) {
1047        return HdmiConfig.HIDE_DEVICES_BEHIND_LEGACY_SWITCH
1048                && !isConnectedToCecSwitch(info.getPhysicalAddress(), mCecSwitches);
1049    }
1050
1051    private static boolean isConnectedToCecSwitch(int path, Collection<Integer> switches) {
1052        for (int switchPath : switches) {
1053            if (isParentPath(switchPath, path)) {
1054                return true;
1055            }
1056        }
1057        return false;
1058    }
1059
1060    private static boolean isParentPath(int parentPath, int childPath) {
1061        // (A000, AB00) (AB00, ABC0), (ABC0, ABCD)
1062        // If child's last non-zero nibble is removed, the result equals to the parent.
1063        for (int i = 0; i <= 12; i += 4) {
1064            int nibble = (childPath >> i) & 0xF;
1065            if (nibble != 0) {
1066                int parentNibble = (parentPath >> i) & 0xF;
1067                return parentNibble == 0 && (childPath >> i+4) == (parentPath >> i+4);
1068            }
1069        }
1070        return false;
1071    }
1072
1073    private void invokeDeviceEventListener(HdmiDeviceInfo info, boolean activated) {
1074        if (!hideDevicesBehindLegacySwitch(info)) {
1075            mService.invokeDeviceEventListeners(info, activated);
1076        }
1077    }
1078
1079    @ServiceThreadOnly
1080    private boolean isLocalDeviceAddress(int address) {
1081        assertRunOnServiceThread();
1082        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
1083            if (device.isAddressOf(address)) {
1084                return true;
1085            }
1086        }
1087        return false;
1088    }
1089
1090    @ServiceThreadOnly
1091    HdmiDeviceInfo getAvrDeviceInfo() {
1092        assertRunOnServiceThread();
1093        return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1094    }
1095
1096    /**
1097     * Return a {@link HdmiDeviceInfo} corresponding to the given {@code logicalAddress}.
1098     *
1099     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
1100     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
1101     *
1102     * @param logicalAddress logical address to be retrieved
1103     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1104     *         Returns null if no logical address matched
1105     */
1106    @ServiceThreadOnly
1107    HdmiDeviceInfo getDeviceInfo(int logicalAddress) {
1108        assertRunOnServiceThread();
1109        return mDeviceInfos.get(logicalAddress);
1110    }
1111
1112    boolean hasSystemAudioDevice() {
1113        return getSafeAvrDeviceInfo() != null;
1114    }
1115
1116    HdmiDeviceInfo getSafeAvrDeviceInfo() {
1117        return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
1118    }
1119
1120    /**
1121     * Thread safe version of {@link #getDeviceInfo(int)}.
1122     *
1123     * @param logicalAddress logical address to be retrieved
1124     * @return {@link HdmiDeviceInfo} matched with the given {@code logicalAddress}.
1125     *         Returns null if no logical address matched
1126     */
1127    HdmiDeviceInfo getSafeDeviceInfo(int logicalAddress) {
1128        synchronized (mLock) {
1129            return mSafeAllDeviceInfos.get(logicalAddress);
1130        }
1131    }
1132
1133    /**
1134     * Called when a device is newly added or a new device is detected or
1135     * existing device is updated.
1136     *
1137     * @param info device info of a new device.
1138     */
1139    @ServiceThreadOnly
1140    final void addCecDevice(HdmiDeviceInfo info) {
1141        assertRunOnServiceThread();
1142        addDeviceInfo(info);
1143        if (info.getLogicalAddress() == mAddress) {
1144            // The addition of TV device itself should not be notified.
1145            return;
1146        }
1147        invokeDeviceEventListener(info, true);
1148    }
1149
1150    /**
1151     * Called when a device is removed or removal of device is detected.
1152     *
1153     * @param address a logical address of a device to be removed
1154     */
1155    @ServiceThreadOnly
1156    final void removeCecDevice(int address) {
1157        assertRunOnServiceThread();
1158        HdmiDeviceInfo info = removeDeviceInfo(address);
1159
1160        mCecMessageCache.flushMessagesFrom(address);
1161        invokeDeviceEventListener(info, false);
1162    }
1163
1164    @ServiceThreadOnly
1165    void handleRemoveActiveRoutingPath(int path) {
1166        assertRunOnServiceThread();
1167        // Seq #23
1168        if (isTailOfActivePath(path, getActivePath())) {
1169            removeAction(RoutingControlAction.class);
1170            int newPath = mService.portIdToPath(getActivePortId());
1171            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
1172                    mAddress, getActivePath(), newPath));
1173            addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null));
1174        }
1175    }
1176
1177    /**
1178     * Launch routing control process.
1179     *
1180     * @param routingForBootup true if routing control is initiated due to One Touch Play
1181     *        or TV power on
1182     */
1183    @ServiceThreadOnly
1184    void launchRoutingControl(boolean routingForBootup) {
1185        assertRunOnServiceThread();
1186        // Seq #24
1187        if (getActivePortId() != Constants.INVALID_PORT_ID) {
1188            if (!routingForBootup && !isProhibitMode()) {
1189                removeAction(RoutingControlAction.class);
1190                int newPath = mService.portIdToPath(getActivePortId());
1191                setActivePath(newPath);
1192                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
1193                        getActivePath(), newPath));
1194                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1195                        routingForBootup, null));
1196            }
1197        } else {
1198            int activePath = mService.getPhysicalAddress();
1199            setActivePath(activePath);
1200            if (!routingForBootup) {
1201                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1202                        activePath));
1203            }
1204        }
1205    }
1206
1207    /**
1208     * Returns the {@link HdmiDeviceInfo} instance whose physical address matches
1209     * the given routing path. CEC devices use routing path for its physical address to
1210     * describe the hierarchy of the devices in the network.
1211     *
1212     * @param path routing path or physical address
1213     * @return {@link HdmiDeviceInfo} if the matched info is found; otherwise null
1214     */
1215    @ServiceThreadOnly
1216    final HdmiDeviceInfo getDeviceInfoByPath(int path) {
1217        assertRunOnServiceThread();
1218        for (HdmiDeviceInfo info : getDeviceInfoList(false)) {
1219            if (info.getPhysicalAddress() == path) {
1220                return info;
1221            }
1222        }
1223        return null;
1224    }
1225
1226    /**
1227     * Whether a device of the specified physical address and logical address exists
1228     * in a device info list. However, both are minimal condition and it could
1229     * be different device from the original one.
1230     *
1231     * @param logicalAddress logical address of a device to be searched
1232     * @param physicalAddress physical address of a device to be searched
1233     * @return true if exist; otherwise false
1234     */
1235    @ServiceThreadOnly
1236    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1237        assertRunOnServiceThread();
1238        HdmiDeviceInfo device = getDeviceInfo(logicalAddress);
1239        if (device == null) {
1240            return false;
1241        }
1242        return device.getPhysicalAddress() == physicalAddress;
1243    }
1244
1245    @Override
1246    @ServiceThreadOnly
1247    void onHotplug(int portId, boolean connected) {
1248        assertRunOnServiceThread();
1249
1250        if (!connected) {
1251            removeCecSwitches(portId);
1252        }
1253        // Tv device will have permanent HotplugDetectionAction.
1254        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1255        if (!hotplugActions.isEmpty()) {
1256            // Note that hotplug action is single action running on a machine.
1257            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1258            // It covers seq #40, #43.
1259            hotplugActions.get(0).pollAllDevicesNow();
1260        }
1261    }
1262
1263    private void removeCecSwitches(int portId) {
1264        Iterator<Integer> it = mCecSwitches.iterator();
1265        while (!it.hasNext()) {
1266            int path = it.next();
1267            if (pathToPortId(path) == portId) {
1268                it.remove();
1269            }
1270        }
1271    }
1272
1273    @ServiceThreadOnly
1274    void setAutoDeviceOff(boolean enabled) {
1275        assertRunOnServiceThread();
1276        mAutoDeviceOff = enabled;
1277    }
1278
1279    @ServiceThreadOnly
1280    void setAutoWakeup(boolean enabled) {
1281        assertRunOnServiceThread();
1282        mAutoWakeup = enabled;
1283    }
1284
1285    @ServiceThreadOnly
1286    boolean getAutoWakeup() {
1287        assertRunOnServiceThread();
1288        return mAutoWakeup;
1289    }
1290
1291    @Override
1292    @ServiceThreadOnly
1293    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1294        super.disableDevice(initiatedByCec, callback);
1295        assertRunOnServiceThread();
1296        // Remove any repeated working actions.
1297        // HotplugDetectionAction will be reinstated during the wake up process.
1298        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1299        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1300        removeAction(DeviceDiscoveryAction.class);
1301        removeAction(HotplugDetectionAction.class);
1302        removeAction(PowerStatusMonitorAction.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    void updateDevicePowerStatus(int logicalAddress, int newPowerStatus) {
1526        HdmiDeviceInfo info = getDeviceInfo(logicalAddress);
1527        if (info == null) {
1528            Slog.w(TAG, "Can not update power status of non-existing device:" + logicalAddress);
1529            return;
1530        }
1531
1532        if (info.getDevicePowerStatus() == newPowerStatus) {
1533            return;
1534        }
1535
1536        HdmiDeviceInfo newInfo = HdmiUtils.cloneHdmiDeviceInfo(info, newPowerStatus);
1537        // addDeviceInfo replaces old device info with new one if exists.
1538        addDeviceInfo(newInfo);
1539
1540        // TODO: notify this update to others.
1541    }
1542}
1543