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