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