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