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