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