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