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