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