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