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