HdmiCecLocalDeviceTv.java revision c516d65fd96cdc39f9935ddb80d26ee6499a77bf
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    // Seq #32
540    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
541        assertRunOnServiceThread();
542        if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
543            setSystemAudioMode(false, true);
544            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
545            return;
546        }
547        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
548        if (avr == null) {
549            setSystemAudioMode(false, true);
550            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
551            return;
552        }
553
554        addAndStartAction(
555                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
556    }
557
558    // # Seq 25
559    void setSystemAudioMode(boolean on, boolean updateSetting) {
560        synchronized (mLock) {
561            if (on != mSystemAudioMode) {
562                mSystemAudioMode = on;
563                if (updateSetting) {
564                    mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
565                }
566                mService.announceSystemAudioModeChange(on);
567            }
568        }
569    }
570
571    boolean getSystemAudioMode() {
572        synchronized (mLock) {
573            return mSystemAudioMode;
574        }
575    }
576
577    /**
578     * Change ARC status into the given {@code enabled} status.
579     *
580     * @return {@code true} if ARC was in "Enabled" status
581     */
582    @ServiceThreadOnly
583    boolean setArcStatus(boolean enabled) {
584        assertRunOnServiceThread();
585        boolean oldStatus = mArcEstablished;
586        // 1. Enable/disable ARC circuit.
587        mService.setAudioReturnChannel(enabled);
588        // 2. Notify arc status to audio service.
589        notifyArcStatusToAudioService(enabled);
590        // 3. Update arc status;
591        mArcEstablished = enabled;
592        return oldStatus;
593    }
594
595    private void notifyArcStatusToAudioService(boolean enabled) {
596        // Note that we don't set any name to ARC.
597        mService.getAudioManager().setWiredDeviceConnectionState(
598                AudioSystem.DEVICE_OUT_HDMI_ARC,
599                enabled ? 1 : 0, "");
600    }
601
602    /**
603     * Returns whether ARC is enabled or not.
604     */
605    @ServiceThreadOnly
606    boolean isArcEstabilished() {
607        assertRunOnServiceThread();
608        return mArcFeatureEnabled && mArcEstablished;
609    }
610
611    @ServiceThreadOnly
612    void changeArcFeatureEnabled(boolean enabled) {
613        assertRunOnServiceThread();
614
615        if (mArcFeatureEnabled != enabled) {
616            if (enabled) {
617                if (!mArcEstablished) {
618                    startArcAction(true);
619                }
620            } else {
621                if (mArcEstablished) {
622                    startArcAction(false);
623                }
624            }
625            mArcFeatureEnabled = enabled;
626        }
627    }
628
629    @ServiceThreadOnly
630    boolean isArcFeatureEnabled() {
631        assertRunOnServiceThread();
632        return mArcFeatureEnabled;
633    }
634
635    @ServiceThreadOnly
636    private void startArcAction(boolean enabled) {
637        assertRunOnServiceThread();
638        HdmiCecDeviceInfo info = getAvrDeviceInfo();
639        if (info == null) {
640            return;
641        }
642        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
643            return;
644        }
645
646        // Terminate opposite action and start action if not exist.
647        if (enabled) {
648            removeAction(RequestArcTerminationAction.class);
649            if (!hasAction(RequestArcInitiationAction.class)) {
650                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
651            }
652        } else {
653            removeAction(RequestArcInitiationAction.class);
654            if (!hasAction(RequestArcTerminationAction.class)) {
655                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
656            }
657        }
658    }
659
660    void setAudioStatus(boolean mute, int volume) {
661        synchronized (mLock) {
662            mSystemAudioMute = mute;
663            mSystemAudioVolume = volume;
664            // TODO: pass volume to service (audio service) after scale it to local volume level.
665            mService.setAudioStatus(mute, volume);
666        }
667    }
668
669    @ServiceThreadOnly
670    void changeVolume(int curVolume, int delta, int maxVolume) {
671        assertRunOnServiceThread();
672        if (delta == 0 || !isSystemAudioOn()) {
673            return;
674        }
675
676        int targetVolume = curVolume + delta;
677        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
678        synchronized (mLock) {
679            // If new volume is the same as current system audio volume, just ignore it.
680            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
681            if (cecVolume == mSystemAudioVolume) {
682                // Update tv volume with system volume value.
683                mService.setAudioStatus(false,
684                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
685                return;
686            }
687        }
688
689        // Remove existing volume action.
690        removeAction(VolumeControlAction.class);
691
692        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
693        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
694                cecVolume, delta > 0));
695    }
696
697    @ServiceThreadOnly
698    void changeMute(boolean mute) {
699        assertRunOnServiceThread();
700        if (!isSystemAudioOn()) {
701            return;
702        }
703
704        // Remove existing volume action.
705        removeAction(VolumeControlAction.class);
706        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
707        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
708    }
709
710    private boolean isSystemAudioOn() {
711        if (getAvrDeviceInfo() == null) {
712            return false;
713        }
714
715        synchronized (mLock) {
716            return mSystemAudioMode;
717        }
718    }
719
720    @Override
721    @ServiceThreadOnly
722    protected boolean handleInitiateArc(HdmiCecMessage message) {
723        assertRunOnServiceThread();
724        // In case where <Initiate Arc> is started by <Request ARC Initiation>
725        // need to clean up RequestArcInitiationAction.
726        removeAction(RequestArcInitiationAction.class);
727        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
728                message.getSource(), true);
729        addAndStartAction(action);
730        return true;
731    }
732
733    @Override
734    @ServiceThreadOnly
735    protected boolean handleTerminateArc(HdmiCecMessage message) {
736        assertRunOnServiceThread();
737        // In case where <Terminate Arc> is started by <Request ARC Termination>
738        // need to clean up RequestArcInitiationAction.
739        removeAction(RequestArcTerminationAction.class);
740        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
741                message.getSource(), false);
742        addAndStartAction(action);
743        return true;
744    }
745
746    @Override
747    @ServiceThreadOnly
748    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
749        assertRunOnServiceThread();
750        if (!isMessageForSystemAudio(message)) {
751            return false;
752        }
753        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
754                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
755        addAndStartAction(action);
756        return true;
757    }
758
759    @Override
760    @ServiceThreadOnly
761    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
762        assertRunOnServiceThread();
763        if (!isMessageForSystemAudio(message)) {
764            return false;
765        }
766        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
767        return true;
768    }
769
770    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
771        if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM
772                || message.getDestination() != Constants.ADDR_TV
773                || getAvrDeviceInfo() == null) {
774            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
775            return false;
776        }
777        return true;
778    }
779
780    /**
781     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
782     * logical address as new device info's.
783     *
784     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
785     *
786     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
787     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
788     *         that has the same logical address as new one has.
789     */
790    @ServiceThreadOnly
791    private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
792        assertRunOnServiceThread();
793        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
794        if (oldDeviceInfo != null) {
795            removeDeviceInfo(deviceInfo.getLogicalAddress());
796        }
797        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
798        updateSafeDeviceInfoList();
799        return oldDeviceInfo;
800    }
801
802    /**
803     * Remove a device info corresponding to the given {@code logicalAddress}.
804     * It returns removed {@link HdmiCecDeviceInfo} if exists.
805     *
806     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
807     *
808     * @param logicalAddress logical address of device to be removed
809     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
810     */
811    @ServiceThreadOnly
812    private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
813        assertRunOnServiceThread();
814        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
815        if (deviceInfo != null) {
816            mDeviceInfos.remove(logicalAddress);
817        }
818        updateSafeDeviceInfoList();
819        return deviceInfo;
820    }
821
822    /**
823     * Return a list of all {@link HdmiCecDeviceInfo}.
824     *
825     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
826     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
827     */
828    @ServiceThreadOnly
829    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
830        assertRunOnServiceThread();
831        if (includelLocalDevice) {
832            return HdmiUtils.sparseArrayToList(mDeviceInfos);
833        } else {
834            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
835            for (int i = 0; i < mDeviceInfos.size(); ++i) {
836                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
837                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
838                    infoList.add(info);
839                }
840            }
841            return infoList;
842        }
843    }
844
845    /**
846     * Return external input devices.
847     */
848    List<HdmiCecDeviceInfo> getSafeExternalInputs() {
849        synchronized (mLock) {
850            return mSafeExternalInputs;
851        }
852    }
853
854    @ServiceThreadOnly
855    private void updateSafeDeviceInfoList() {
856        assertRunOnServiceThread();
857        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
858        List<HdmiCecDeviceInfo> externalInputs = getInputDevices();
859        synchronized (mLock) {
860            mSafeAllDeviceInfos = copiedDevices;
861            mSafeExternalInputs = externalInputs;
862        }
863    }
864
865    /**
866     * Return a list of external cec input (source) devices.
867     *
868     * <p>Note that this effectively excludes non-source devices like system audio,
869     * secondary TV.
870     */
871    private List<HdmiCecDeviceInfo> getInputDevices() {
872        ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
873        for (int i = 0; i < mDeviceInfos.size(); ++i) {
874            HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
875            if (isLocalDeviceAddress(i)) {
876                continue;
877            }
878            if (info.isSourceType()) {
879                infoList.add(info);
880            }
881        }
882        return infoList;
883    }
884
885    @ServiceThreadOnly
886    private boolean isLocalDeviceAddress(int address) {
887        assertRunOnServiceThread();
888        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
889            if (device.isAddressOf(address)) {
890                return true;
891            }
892        }
893        return false;
894    }
895
896    @ServiceThreadOnly
897    HdmiCecDeviceInfo getAvrDeviceInfo() {
898        assertRunOnServiceThread();
899        return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
900    }
901
902    /**
903     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
904     *
905     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
906     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
907     *
908     * @param logicalAddress logical address to be retrieved
909     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
910     *         Returns null if no logical address matched
911     */
912    @ServiceThreadOnly
913    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
914        assertRunOnServiceThread();
915        return mDeviceInfos.get(logicalAddress);
916    }
917
918    boolean hasSystemAudioDevice() {
919        return getSafeAvrDeviceInfo() != null;
920    }
921
922    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
923        return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
924    }
925
926    /**
927     * Thread safe version of {@link #getDeviceInfo(int)}.
928     *
929     * @param logicalAddress logical address to be retrieved
930     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
931     *         Returns null if no logical address matched
932     */
933    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
934        synchronized (mLock) {
935            return mSafeAllDeviceInfos.get(logicalAddress);
936        }
937    }
938
939    /**
940     * Called when a device is newly added or a new device is detected or
941     * existing device is updated.
942     *
943     * @param info device info of a new device.
944     */
945    @ServiceThreadOnly
946    final void addCecDevice(HdmiCecDeviceInfo info) {
947        assertRunOnServiceThread();
948        addDeviceInfo(info);
949        if (info.getLogicalAddress() == mAddress) {
950            // The addition of TV device itself should not be notified.
951            return;
952        }
953        mService.invokeDeviceEventListeners(info, true);
954    }
955
956    /**
957     * Called when a device is removed or removal of device is detected.
958     *
959     * @param address a logical address of a device to be removed
960     */
961    @ServiceThreadOnly
962    final void removeCecDevice(int address) {
963        assertRunOnServiceThread();
964        HdmiCecDeviceInfo info = removeDeviceInfo(address);
965
966        mCecMessageCache.flushMessagesFrom(address);
967        mService.invokeDeviceEventListeners(info, false);
968    }
969
970    @ServiceThreadOnly
971    void handleRemoveActiveRoutingPath(int path) {
972        assertRunOnServiceThread();
973        // Seq #23
974        if (isTailOfActivePath(path, getActivePath())) {
975            removeAction(RoutingControlAction.class);
976            int newPath = mService.portIdToPath(getActivePortId());
977            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
978                    mAddress, getActivePath(), newPath));
979            addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null));
980        }
981    }
982
983    /**
984     * Launch routing control process.
985     *
986     * @param routingForBootup true if routing control is initiated due to One Touch Play
987     *        or TV power on
988     */
989    @ServiceThreadOnly
990    void launchRoutingControl(boolean routingForBootup) {
991        assertRunOnServiceThread();
992        // Seq #24
993        if (getActivePortId() != Constants.INVALID_PORT_ID) {
994            if (!routingForBootup && !isProhibitMode()) {
995                removeAction(RoutingControlAction.class);
996                int newPath = mService.portIdToPath(getActivePortId());
997                setActivePath(newPath);
998                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
999                        getActivePath(), newPath));
1000                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1001                        routingForBootup, null));
1002            }
1003        } else {
1004            int activePath = mService.getPhysicalAddress();
1005            setActivePath(activePath);
1006            if (!routingForBootup) {
1007                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1008                        activePath));
1009            }
1010        }
1011    }
1012
1013    /**
1014     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
1015     * the given routing path. CEC devices use routing path for its physical address to
1016     * describe the hierarchy of the devices in the network.
1017     *
1018     * @param path routing path or physical address
1019     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
1020     */
1021    @ServiceThreadOnly
1022    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
1023        assertRunOnServiceThread();
1024        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
1025            if (info.getPhysicalAddress() == path) {
1026                return info;
1027            }
1028        }
1029        return null;
1030    }
1031
1032    /**
1033     * Whether a device of the specified physical address and logical address exists
1034     * in a device info list. However, both are minimal condition and it could
1035     * be different device from the original one.
1036     *
1037     * @param logicalAddress logical address of a device to be searched
1038     * @param physicalAddress physical address of a device to be searched
1039     * @return true if exist; otherwise false
1040     */
1041    @ServiceThreadOnly
1042    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1043        assertRunOnServiceThread();
1044        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
1045        if (device == null) {
1046            return false;
1047        }
1048        return device.getPhysicalAddress() == physicalAddress;
1049    }
1050
1051    @Override
1052    @ServiceThreadOnly
1053    void onHotplug(int portId, boolean connected) {
1054        assertRunOnServiceThread();
1055
1056        // Tv device will have permanent HotplugDetectionAction.
1057        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1058        if (!hotplugActions.isEmpty()) {
1059            // Note that hotplug action is single action running on a machine.
1060            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1061            // It covers seq #40, #43.
1062            hotplugActions.get(0).pollAllDevicesNow();
1063        }
1064    }
1065
1066    @ServiceThreadOnly
1067    void setAutoDeviceOff(boolean enabled) {
1068        assertRunOnServiceThread();
1069        mAutoDeviceOff = enabled;
1070    }
1071
1072    @Override
1073    @ServiceThreadOnly
1074    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1075        super.disableDevice(initiatedByCec, callback);
1076        assertRunOnServiceThread();
1077        // Remove any repeated working actions.
1078        // HotplugDetectionAction will be reinstated during the wake up process.
1079        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1080        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1081        removeAction(DeviceDiscoveryAction.class);
1082        removeAction(HotplugDetectionAction.class);
1083
1084        disableSystemAudioIfExist();
1085        disableArcIfExist();
1086        checkIfPendingActionsCleared();
1087    }
1088
1089    @ServiceThreadOnly
1090    private void disableSystemAudioIfExist() {
1091        assertRunOnServiceThread();
1092        if (getAvrDeviceInfo() == null) {
1093            return;
1094        }
1095
1096        // Seq #31.
1097        removeAction(SystemAudioActionFromAvr.class);
1098        removeAction(SystemAudioActionFromTv.class);
1099        removeAction(SystemAudioAutoInitiationAction.class);
1100        removeAction(SystemAudioStatusAction.class);
1101        removeAction(VolumeControlAction.class);
1102
1103        // Turn off the mode but do not write it the settings, so that the next time TV powers on
1104        // the system audio mode setting can be restored automatically.
1105        setSystemAudioMode(false, false);
1106    }
1107
1108    @ServiceThreadOnly
1109    private void disableArcIfExist() {
1110        assertRunOnServiceThread();
1111        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
1112        if (avr == null) {
1113            return;
1114        }
1115
1116        // Seq #44.
1117        removeAction(RequestArcInitiationAction.class);
1118        if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1119            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1120        }
1121    }
1122
1123    @Override
1124    @ServiceThreadOnly
1125    protected void onStandby(boolean initiatedByCec) {
1126        assertRunOnServiceThread();
1127        // Seq #11
1128        if (!mService.isControlEnabled()) {
1129            return;
1130        }
1131        if (!initiatedByCec) {
1132            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1133                    mAddress, Constants.ADDR_BROADCAST));
1134        }
1135    }
1136
1137    @Override
1138    @ServiceThreadOnly
1139    protected boolean handleStandby(HdmiCecMessage message) {
1140        assertRunOnServiceThread();
1141        // Seq #12
1142        // Tv accepts directly addressed <Standby> only.
1143        if (message.getDestination() == mAddress) {
1144            super.handleStandby(message);
1145        }
1146        return false;
1147    }
1148
1149    boolean isProhibitMode() {
1150        return mService.isProhibitMode();
1151    }
1152
1153    boolean isPowerStandbyOrTransient() {
1154        return mService.isPowerStandbyOrTransient();
1155    }
1156
1157    void displayOsd(int messageId) {
1158        Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE);
1159        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId);
1160        mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
1161                HdmiControlService.PERMISSION);
1162    }
1163}
1164