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