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