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