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