HdmiCecLocalDeviceTv.java revision 6f34f5ab8ab1b1db7887e5405d8b0031e105ab05
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.util.Slog;
30import android.util.SparseArray;
31
32import com.android.internal.annotations.GuardedBy;
33import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
34import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
35
36import java.io.UnsupportedEncodingException;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.List;
40import java.util.Locale;
41
42/**
43 * Represent a logical device of type TV residing in Android system.
44 */
45final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
46    private static final String TAG = "HdmiCecLocalDeviceTv";
47
48    // Whether ARC is available or not. "true" means that ARC is estabilished between TV and
49    // AVR as audio receiver.
50    @ServiceThreadOnly
51    private boolean mArcEstablished = false;
52
53    // Whether ARC feature is enabled or not.
54    private boolean mArcFeatureEnabled = false;
55
56    // Whether SystemAudioMode is "On" or not.
57    @GuardedBy("mLock")
58    private boolean mSystemAudioMode;
59
60    // The previous port id (input) before switching to the new one. This is remembered in order to
61    // be able to switch to it upon receiving <Inactive Source> from currently active source.
62    // This remains valid only when the active source was switched via one touch play operation
63    // (either by TV or source device). Manual port switching invalidates this value to
64    // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything.
65    @GuardedBy("mLock")
66    private int mPrevPortId;
67
68    @GuardedBy("mLock")
69    private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME;
70
71    @GuardedBy("mLock")
72    private boolean mSystemAudioMute = false;
73
74    // Copy of mDeviceInfos to guarantee thread-safety.
75    @GuardedBy("mLock")
76    private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList();
77    // All external cec input(source) devices. Does not include system audio device.
78    @GuardedBy("mLock")
79    private List<HdmiCecDeviceInfo> mSafeExternalInputs = Collections.emptyList();
80
81    // Map-like container of all cec devices including local ones.
82    // A logical address of device is used as key of container.
83    // This is not thread-safe. For external purpose use mSafeDeviceInfos.
84    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
85
86    // If true, TV going to standby mode puts other devices also to standby.
87    private boolean mAutoDeviceOff;
88
89    HdmiCecLocalDeviceTv(HdmiControlService service) {
90        super(service, HdmiCecDeviceInfo.DEVICE_TV);
91        mPrevPortId = Constants.INVALID_PORT_ID;
92        // TODO: load system audio mode and set it to mSystemAudioMode.
93    }
94
95    @Override
96    @ServiceThreadOnly
97    protected void onAddressAllocated(int logicalAddress) {
98        assertRunOnServiceThread();
99        // TODO: vendor-specific initialization here.
100
101        mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
102                mAddress, mService.getPhysicalAddress(), mDeviceType));
103        mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
104                mAddress, mService.getVendorId()));
105        launchRoutingControl(true);
106        launchDeviceDiscovery();
107
108        registerAudioPortUpdateListener();
109        // TODO: unregister audio port update listener if local device is released.
110    }
111
112    private void registerAudioPortUpdateListener() {
113        mService.getAudioManager().registerAudioPortUpdateListener(
114                new OnAudioPortUpdateListener() {
115                    @Override
116                    public void OnAudioPatchListUpdate(AudioPatch[] patchList) {}
117
118                    @Override
119                    public void OnAudioPortListUpdate(AudioPort[] portList) {
120                        if (!mSystemAudioMode) {
121                            return;
122                        }
123                        int devices = mService.getAudioManager().getDevicesForStream(
124                                AudioSystem.STREAM_MUSIC);
125                        if ((devices & ~AudioSystem.DEVICE_ALL_HDMI_SYSTEM_AUDIO_AND_SPEAKER)
126                                != 0) {
127                            // TODO: release system audio here.
128                        }
129                    }
130
131                    @Override
132                    public void OnServiceDied() {}
133                });
134    }
135
136    /**
137     * Performs the action 'device select', or 'one touch play' initiated by TV.
138     *
139     * @param targetAddress logical address of the device to select
140     * @param callback callback object to report the result with
141     */
142    @ServiceThreadOnly
143    void deviceSelect(int targetAddress, IHdmiControlCallback callback) {
144        assertRunOnServiceThread();
145        if (targetAddress == Constants.ADDR_INTERNAL) {
146            handleSelectInternalSource(callback);
147            return;
148        }
149        HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress);
150        if (targetDevice == null) {
151            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
152            return;
153        }
154        removeAction(DeviceSelectAction.class);
155        addAndStartAction(new DeviceSelectAction(this, targetDevice, callback));
156    }
157
158    @ServiceThreadOnly
159    private void handleSelectInternalSource(IHdmiControlCallback callback) {
160        assertRunOnServiceThread();
161        // Seq #18
162        if (mService.isControlEnabled() && getActiveSource() != mAddress) {
163            updateActiveSource(mAddress, mService.getPhysicalAddress());
164            // TODO: Check if this comes from <Text/Image View On> - if true, do nothing.
165            HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(
166                    mAddress, mService.getPhysicalAddress());
167            mService.sendCecCommand(activeSource);
168        }
169    }
170
171    @ServiceThreadOnly
172    void updateActiveSource(int activeSource, int activePath) {
173        assertRunOnServiceThread();
174        // Seq #14
175        if (activeSource == getActiveSource() && activePath == getActivePath()) {
176            return;
177        }
178        setActiveSource(activeSource);
179        setActivePath(activePath);
180        if (getDeviceInfo(activeSource) != null && activeSource != mAddress) {
181            if (mService.pathToPortId(activePath) == getActivePortId()) {
182                setPrevPortId(getActivePortId());
183            }
184            // TODO: Show the OSD banner related to the new active source device.
185        } else {
186            // TODO: If displayed, remove the OSD banner related to the previous
187            //       active source device.
188        }
189    }
190
191    /**
192     * Returns the previous port id kept to handle input switching on <Inactive Source>.
193     */
194    int getPrevPortId() {
195        synchronized (mLock) {
196            return mPrevPortId;
197        }
198    }
199
200    /**
201     * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be
202     * taken for <Inactive Source>.
203     */
204    void setPrevPortId(int portId) {
205        synchronized (mLock) {
206            mPrevPortId = portId;
207        }
208    }
209
210    @ServiceThreadOnly
211    void updateActivePortId(int portId) {
212        assertRunOnServiceThread();
213        // Seq #15
214        if (portId == getActivePortId()) {
215            return;
216        }
217        setPrevPortId(portId);
218        // TODO: Actually switch the physical port here. Handle PAP/PIP as well.
219        //       Show OSD port change banner
220        mService.invokeInputChangeListener(getActiveSource());
221    }
222
223    @ServiceThreadOnly
224    void doManualPortSwitching(int portId, IHdmiControlCallback callback) {
225        assertRunOnServiceThread();
226        // Seq #20
227        if (!mService.isControlEnabled() || portId == getActivePortId()) {
228            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
229            return;
230        }
231        // TODO: Make sure this call does not stem from <Active Source> message reception.
232
233        setActivePortId(portId);
234        // TODO: Return immediately if the operation is triggered by <Text/Image View On>
235        //       and this is the first notification about the active input after power-on.
236        // TODO: Handle invalid port id / active input which should be treated as an
237        //       internal tuner.
238
239        removeAction(RoutingControlAction.class);
240
241        int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId()));
242        int newPath = mService.portIdToPath(portId);
243        HdmiCecMessage routingChange =
244                HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath);
245        mService.sendCecCommand(routingChange);
246        addAndStartAction(new RoutingControlAction(this, newPath, false, callback));
247    }
248
249    int getPowerStatus() {
250        return mService.getPowerStatus();
251    }
252
253    /**
254     * Sends key to a target CEC device.
255     *
256     * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}.
257     * @param isPressed true if this is key press event
258     */
259    @Override
260    @ServiceThreadOnly
261    protected void sendKeyEvent(int keyCode, boolean isPressed) {
262        assertRunOnServiceThread();
263        List<SendKeyAction> action = getActions(SendKeyAction.class);
264        if (!action.isEmpty()) {
265            action.get(0).processKeyEvent(keyCode, isPressed);
266        } else {
267            if (isPressed) {
268                addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode));
269            } else {
270                Slog.w(TAG, "Discard key release event");
271            }
272        }
273    }
274
275    private static void invokeCallback(IHdmiControlCallback callback, int result) {
276        if (callback == null) {
277            return;
278        }
279        try {
280            callback.onComplete(result);
281        } catch (RemoteException e) {
282            Slog.e(TAG, "Invoking callback failed:" + e);
283        }
284    }
285
286    @Override
287    @ServiceThreadOnly
288    protected boolean handleActiveSource(HdmiCecMessage message) {
289        assertRunOnServiceThread();
290        int address = message.getSource();
291        int path = HdmiUtils.twoBytesToInt(message.getParams());
292        if (getDeviceInfo(address) == null) {
293            handleNewDeviceAtTheTailOfActivePath(path);
294        } else {
295            ActiveSourceHandler.create(this, null).process(address, path);
296        }
297        return true;
298    }
299
300    @Override
301    @ServiceThreadOnly
302    protected boolean handleInactiveSource(HdmiCecMessage message) {
303        assertRunOnServiceThread();
304        // Seq #10
305
306        // Ignore <Inactive Source> from non-active source device.
307        if (getActiveSource() != message.getSource()) {
308            return true;
309        }
310        if (isProhibitMode()) {
311            return true;
312        }
313        int portId = getPrevPortId();
314        if (portId != Constants.INVALID_PORT_ID) {
315            // TODO: Do this only if TV is not showing multiview like PIP/PAP.
316
317            HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource());
318            if (inactiveSource == null) {
319                return true;
320            }
321            if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) {
322                return true;
323            }
324            // TODO: Switch the TV freeze mode off
325
326            setActivePortId(portId);
327            doManualPortSwitching(portId, null);
328            setPrevPortId(Constants.INVALID_PORT_ID);
329        }
330        return true;
331    }
332
333    @Override
334    @ServiceThreadOnly
335    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
336        assertRunOnServiceThread();
337        // Seq #19
338        if (mAddress == getActiveSource()) {
339            mService.sendCecCommand(
340                    HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath()));
341        }
342        return true;
343    }
344
345    @Override
346    @ServiceThreadOnly
347    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
348        assertRunOnServiceThread();
349        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
350                mAddress, Locale.getDefault().getISO3Language());
351        // TODO: figure out how to handle failed to get language code.
352        if (command != null) {
353            mService.sendCecCommand(command);
354        } else {
355            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
356        }
357        return true;
358    }
359
360    @Override
361    @ServiceThreadOnly
362    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
363        assertRunOnServiceThread();
364        // Ignore if [Device Discovery Action] is going on.
365        if (hasAction(DeviceDiscoveryAction.class)) {
366            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
367                    + "because Device Discovery Action is on-going:" + message);
368            return true;
369        }
370
371        int path = HdmiUtils.twoBytesToInt(message.getParams());
372        int address = message.getSource();
373        if (!isInDeviceList(path, address)) {
374            handleNewDeviceAtTheTailOfActivePath(path);
375        }
376        addAndStartAction(new NewDeviceAction(this, address, path));
377        return true;
378    }
379
380    private void handleNewDeviceAtTheTailOfActivePath(int path) {
381        // Seq #22
382        if (isTailOfActivePath(path, getActivePath())) {
383            removeAction(RoutingControlAction.class);
384            int newPath = mService.portIdToPath(getActivePortId());
385            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
386                    mAddress, getActivePath(), newPath));
387            addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null));
388        }
389    }
390
391    /**
392     * Whether the given path is located in the tail of current active path.
393     *
394     * @param path to be tested
395     * @param activePath current active path
396     * @return true if the given path is located in the tail of current active path; otherwise,
397     *         false
398     */
399    static boolean isTailOfActivePath(int path, int activePath) {
400        // If active routing path is internal source, return false.
401        if (activePath == 0) {
402            return false;
403        }
404        for (int i = 12; i >= 0; i -= 4) {
405            int curActivePath = (activePath >> i) & 0xF;
406            if (curActivePath == 0) {
407                return true;
408            } else {
409                int curPath = (path >> i) & 0xF;
410                if (curPath != curActivePath) {
411                    return false;
412                }
413            }
414        }
415        return false;
416    }
417
418    @Override
419    @ServiceThreadOnly
420    protected boolean handleRoutingChange(HdmiCecMessage message) {
421        assertRunOnServiceThread();
422        // Seq #21
423        byte[] params = message.getParams();
424        int currentPath = HdmiUtils.twoBytesToInt(params);
425        if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) {
426            int newPath = HdmiUtils.twoBytesToInt(params, 2);
427            setActivePath(newPath);
428            removeAction(RoutingControlAction.class);
429            addAndStartAction(new RoutingControlAction(this, newPath, true, null));
430        }
431        return true;
432    }
433
434    @Override
435    @ServiceThreadOnly
436    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
437        assertRunOnServiceThread();
438
439        byte params[] = message.getParams();
440        int mute = params[0] & 0x80;
441        int volume = params[0] & 0x7F;
442        setAudioStatus(mute == 0x80, volume);
443        return true;
444    }
445
446    @Override
447    @ServiceThreadOnly
448    protected boolean handleTextViewOn(HdmiCecMessage message) {
449        assertRunOnServiceThread();
450        if (mService.isPowerStandbyOrTransient()) {
451            mService.wakeUp();
452        }
453        // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel
454        //       that represents the source device.
455        return true;
456    }
457
458    @Override
459    @ServiceThreadOnly
460    protected boolean handleImageViewOn(HdmiCecMessage message) {
461        assertRunOnServiceThread();
462        // Currently, it's the same as <Text View On>.
463        return handleTextViewOn(message);
464    }
465
466    @Override
467    @ServiceThreadOnly
468    protected boolean handleSetOsdName(HdmiCecMessage message) {
469        int source = message.getSource();
470        HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source);
471        // If the device is not in device list, ignore it.
472        if (deviceInfo == null) {
473            Slog.e(TAG, "No source device info for <Set Osd Name>." + message);
474            return true;
475        }
476        String osdName = null;
477        try {
478            osdName = new String(message.getParams(), "US-ASCII");
479        } catch (UnsupportedEncodingException e) {
480            Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e);
481            return true;
482        }
483
484        if (deviceInfo.getDisplayName().equals(osdName)) {
485            Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message);
486            return true;
487        }
488
489        addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(),
490                deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(),
491                deviceInfo.getVendorId(), osdName));
492        return true;
493    }
494
495    @ServiceThreadOnly
496    private void launchDeviceDiscovery() {
497        assertRunOnServiceThread();
498        clearDeviceInfoList();
499        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
500                new DeviceDiscoveryCallback() {
501                    @Override
502                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
503                        for (HdmiCecDeviceInfo info : deviceInfos) {
504                            addCecDevice(info);
505                        }
506
507                        // Since we removed all devices when it's start and
508                        // device discovery action does not poll local devices,
509                        // we should put device info of local device manually here
510                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
511                            addCecDevice(device.getDeviceInfo());
512                        }
513
514                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
515
516                        // If there is AVR, initiate System Audio Auto initiation action,
517                        // which turns on and off system audio according to last system
518                        // audio setting.
519                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
520                        if (avrInfo != null) {
521                            addAndStartAction(new SystemAudioAutoInitiationAction(
522                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
523                            if (mArcEstablished) {
524                                startArcAction(true);
525                            }
526                        }
527                    }
528                });
529        addAndStartAction(action);
530    }
531
532    // Clear all device info.
533    @ServiceThreadOnly
534    private void clearDeviceInfoList() {
535        assertRunOnServiceThread();
536        mDeviceInfos.clear();
537        updateSafeDeviceInfoList();
538    }
539
540    @ServiceThreadOnly
541    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
542        assertRunOnServiceThread();
543        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
544        if (avr == null) {
545            invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
546            return;
547        }
548
549        addAndStartAction(
550                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
551    }
552
553    // # Seq 25
554    void setSystemAudioMode(boolean on) {
555        synchronized (mLock) {
556            if (on != mSystemAudioMode) {
557                mSystemAudioMode = on;
558                // TODO: Need to set the preference for SystemAudioMode.
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));
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 onTransitionToStandby(boolean initiatedByCec) {
1068        assertRunOnServiceThread();
1069        // Remove any repeated working actions.
1070        // HotplugDetectionAction will be reinstated during the wake up process.
1071        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1072        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1073        removeAction(HotplugDetectionAction.class);
1074        checkIfPendingActionsCleared();
1075    }
1076
1077    @Override
1078    @ServiceThreadOnly
1079    protected void onStandBy(boolean initiatedByCec) {
1080        assertRunOnServiceThread();
1081        // Seq #11
1082        if (!mService.isControlEnabled()) {
1083            return;
1084        }
1085        if (!initiatedByCec) {
1086            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1087                    mAddress, Constants.ADDR_BROADCAST));
1088        }
1089    }
1090
1091    @Override
1092    @ServiceThreadOnly
1093    protected boolean handleStandby(HdmiCecMessage message) {
1094        assertRunOnServiceThread();
1095        // Seq #12
1096        // Tv accepts directly addressed <Standby> only.
1097        if (message.getDestination() == mAddress) {
1098            super.handleStandby(message);
1099        }
1100        return false;
1101    }
1102
1103    boolean isProhibitMode() {
1104        return mService.isProhibitMode();
1105    }
1106
1107    boolean isPowerStandbyOrTransient() {
1108        return mService.isPowerStandbyOrTransient();
1109    }
1110
1111    void displayOsd(int messageId) {
1112        Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE);
1113        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId);
1114        mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
1115                HdmiControlService.PERMISSION);
1116    }
1117}
1118