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