HdmiCecLocalDeviceTv.java revision 5e3916a6f14545e033ca1dc56d33ba2983c7ee03
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 (isHdmiControlEnabled() && 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 (!isHdmiControlEnabled() || 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 activePath = HdmiUtils.twoBytesToInt(message.getParams());
247        ActiveSourceHandler.create(this, null).process(message.getSource(), activePath);
248        return true;
249    }
250
251    @Override
252    @ServiceThreadOnly
253    protected boolean handleInactiveSource(HdmiCecMessage message) {
254        assertRunOnServiceThread();
255        // Seq #10
256
257        // Ignore <Inactive Source> from non-active source device.
258        if (getActiveSource() != message.getSource()) {
259            return true;
260        }
261        if (isInPresetInstallationMode()) {
262            return true;
263        }
264        int portId = getPrevPortId();
265        if (portId != HdmiConstants.INVALID_PORT_ID) {
266            // TODO: Do this only if TV is not showing multiview like PIP/PAP.
267
268            HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource());
269            if (inactiveSource == null) {
270                return true;
271            }
272            if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) {
273                return true;
274            }
275            // TODO: Switch the TV freeze mode off
276
277            setActivePortId(portId);
278            doManualPortSwitching(portId, null);
279            setPrevPortId(HdmiConstants.INVALID_PORT_ID);
280        }
281        return true;
282    }
283
284    @Override
285    @ServiceThreadOnly
286    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
287        assertRunOnServiceThread();
288        // Seq #19
289        int address = getDeviceInfo().getLogicalAddress();
290        if (address == getActiveSource()) {
291            mService.sendCecCommand(
292                    HdmiCecMessageBuilder.buildActiveSource(address, getActivePath()));
293        }
294        return true;
295    }
296
297    @Override
298    @ServiceThreadOnly
299    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
300        assertRunOnServiceThread();
301        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
302                mAddress, Locale.getDefault().getISO3Language());
303        // TODO: figure out how to handle failed to get language code.
304        if (command != null) {
305            mService.sendCecCommand(command);
306        } else {
307            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
308        }
309        return true;
310    }
311
312    @Override
313    @ServiceThreadOnly
314    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
315        assertRunOnServiceThread();
316        // Ignore if [Device Discovery Action] is going on.
317        if (hasAction(DeviceDiscoveryAction.class)) {
318            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
319                    + "because Device Discovery Action is on-going:" + message);
320            return true;
321        }
322
323        int physicalAddress = HdmiUtils.twoBytesToInt(message.getParams());
324        int logicalAddress = message.getSource();
325
326        // If it is a new device and connected to the tail of active path,
327        // it's required to change routing path.
328        boolean requireRoutingChange = !isInDeviceList(physicalAddress, logicalAddress)
329                && isTailOfActivePath(physicalAddress);
330        addAndStartAction(new NewDeviceAction(this, message.getSource(), physicalAddress,
331                requireRoutingChange));
332        return true;
333    }
334
335    @Override
336    @ServiceThreadOnly
337    protected boolean handleVendorSpecificCommand(HdmiCecMessage message) {
338        assertRunOnServiceThread();
339        List<VendorSpecificAction> actions = Collections.emptyList();
340        // TODO: Call mService.getActions(VendorSpecificAction.class) to get all the actions.
341
342        // We assume that there can be multiple vendor-specific command actions running
343        // at the same time. Pass the message to each action to see if one of them needs it.
344        for (VendorSpecificAction action : actions) {
345            if (action.processCommand(message)) {
346                return true;
347            }
348        }
349        // Handle the message here if it is not already consumed by one of the running actions.
350        // Respond with a appropriate vendor-specific command or <Feature Abort>, or create another
351        // vendor-specific action:
352        //
353        // mService.addAndStartAction(new VendorSpecificAction(mService, mAddress));
354        //
355        // For now, simply reply with <Feature Abort> and mark it consumed by returning true.
356        mService.sendCecCommand(HdmiCecMessageBuilder.buildFeatureAbortCommand(
357                message.getDestination(), message.getSource(), message.getOpcode(),
358                HdmiConstants.ABORT_REFUSED));
359        return true;
360    }
361
362    @Override
363    @ServiceThreadOnly
364    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
365        assertRunOnServiceThread();
366
367        byte params[] = message.getParams();
368        if (params.length < 1) {
369            Slog.w(TAG, "Invalide <Report Audio Status> message:" + message);
370            return true;
371        }
372        int mute = params[0] & 0x80;
373        int volume = params[0] & 0x7F;
374        setAudioStatus(mute == 0x80, volume);
375        return true;
376    }
377
378    @ServiceThreadOnly
379    private void launchDeviceDiscovery() {
380        assertRunOnServiceThread();
381        clearDeviceInfoList();
382        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
383                new DeviceDiscoveryCallback() {
384                    @Override
385                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
386                        for (HdmiCecDeviceInfo info : deviceInfos) {
387                            addCecDevice(info);
388                        }
389
390                        // Since we removed all devices when it's start and
391                        // device discovery action does not poll local devices,
392                        // we should put device info of local device manually here
393                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
394                            addCecDevice(device.getDeviceInfo());
395                        }
396
397                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
398
399                        // If there is AVR, initiate System Audio Auto initiation action,
400                        // which turns on and off system audio according to last system
401                        // audio setting.
402                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
403                        if (avrInfo != null) {
404                            addAndStartAction(new SystemAudioAutoInitiationAction(
405                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
406                            if (isConnectedToArcPort(avrInfo.getPhysicalAddress())) {
407                                addAndStartAction(new RequestArcInitiationAction(
408                                        HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
409                            }
410                        }
411                    }
412                });
413        addAndStartAction(action);
414    }
415
416    // Clear all device info.
417    @ServiceThreadOnly
418    private void clearDeviceInfoList() {
419        assertRunOnServiceThread();
420        mDeviceInfos.clear();
421        updateSafeDeviceInfoList();
422    }
423
424    @ServiceThreadOnly
425    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
426        assertRunOnServiceThread();
427        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
428        if (avr == null) {
429            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
430            return;
431        }
432
433        addAndStartAction(
434                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
435    }
436
437    void setSystemAudioMode(boolean on) {
438        synchronized (mLock) {
439            if (on != mSystemAudioMode) {
440                mSystemAudioMode = on;
441                // TODO: Need to set the preference for SystemAudioMode.
442                mService.announceSystemAudioModeChange(on);
443            }
444        }
445    }
446
447    boolean getSystemAudioMode() {
448        synchronized (mLock) {
449            return mSystemAudioMode;
450        }
451    }
452
453    /**
454     * Change ARC status into the given {@code enabled} status.
455     *
456     * @return {@code true} if ARC was in "Enabled" status
457     */
458    boolean setArcStatus(boolean enabled) {
459        synchronized (mLock) {
460            boolean oldStatus = mArcStatusEnabled;
461            // 1. Enable/disable ARC circuit.
462            mService.setAudioReturnChannel(enabled);
463            // 2. Notify arc status to audio service.
464            notifyArcStatusToAudioService(enabled);
465            // 3. Update arc status;
466            mArcStatusEnabled = enabled;
467            return oldStatus;
468        }
469    }
470
471    private void notifyArcStatusToAudioService(boolean enabled) {
472        // Note that we don't set any name to ARC.
473        mService.getAudioManager().setWiredDeviceConnectionState(
474                AudioSystem.DEVICE_OUT_HDMI_ARC,
475                enabled ? 1 : 0, "");
476    }
477
478    /**
479     * Returns whether ARC is enabled or not.
480     */
481    boolean getArcStatus() {
482        synchronized (mLock) {
483            return mArcStatusEnabled;
484        }
485    }
486
487    void setAudioStatus(boolean mute, int volume) {
488        synchronized (mLock) {
489            mSystemAudioMute = mute;
490            mSystemAudioVolume = volume;
491            // TODO: pass volume to service (audio service) after scale it to local volume level.
492            mService.setAudioStatus(mute, volume);
493        }
494    }
495
496    @ServiceThreadOnly
497    void changeVolume(int curVolume, int delta, int maxVolume) {
498        assertRunOnServiceThread();
499        if (delta == 0 || !isSystemAudioOn()) {
500            return;
501        }
502
503        int targetVolume = curVolume + delta;
504        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
505        synchronized (mLock) {
506            // If new volume is the same as current system audio volume, just ignore it.
507            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
508            if (cecVolume == mSystemAudioVolume) {
509                // Update tv volume with system volume value.
510                mService.setAudioStatus(false,
511                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
512                return;
513            }
514        }
515
516        // Remove existing volume action.
517        removeAction(VolumeControlAction.class);
518
519        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
520        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
521                cecVolume, delta > 0));
522    }
523
524    @ServiceThreadOnly
525    void changeMute(boolean mute) {
526        assertRunOnServiceThread();
527        if (!isSystemAudioOn()) {
528            return;
529        }
530
531        // Remove existing volume action.
532        removeAction(VolumeControlAction.class);
533        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
534        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
535    }
536
537    private boolean isSystemAudioOn() {
538        if (getAvrDeviceInfo() == null) {
539            return false;
540        }
541
542        synchronized (mLock) {
543            return mSystemAudioMode;
544        }
545    }
546
547    @Override
548    @ServiceThreadOnly
549    protected boolean handleInitiateArc(HdmiCecMessage message) {
550        assertRunOnServiceThread();
551        // In case where <Initiate Arc> is started by <Request ARC Initiation>
552        // need to clean up RequestArcInitiationAction.
553        removeAction(RequestArcInitiationAction.class);
554        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
555                message.getSource(), true);
556        addAndStartAction(action);
557        return true;
558    }
559
560    @Override
561    @ServiceThreadOnly
562    protected boolean handleTerminateArc(HdmiCecMessage message) {
563        assertRunOnServiceThread();
564        // In case where <Terminate Arc> is started by <Request ARC Termination>
565        // need to clean up RequestArcInitiationAction.
566        // TODO: check conditions of power status by calling is_connected api
567        // to be added soon.
568        removeAction(RequestArcTerminationAction.class);
569        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
570                message.getSource(), false);
571        addAndStartAction(action);
572        return true;
573    }
574
575    @Override
576    @ServiceThreadOnly
577    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
578        assertRunOnServiceThread();
579        if (!isMessageForSystemAudio(message)) {
580            return false;
581        }
582        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
583                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
584        addAndStartAction(action);
585        return true;
586    }
587
588    @Override
589    @ServiceThreadOnly
590    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
591        assertRunOnServiceThread();
592        if (!isMessageForSystemAudio(message)) {
593            return false;
594        }
595        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
596        return true;
597    }
598
599    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
600        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
601                || message.getDestination() != HdmiCec.ADDR_TV
602                || getAvrDeviceInfo() == null) {
603            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
604            return false;
605        }
606        return true;
607    }
608
609    /**
610     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
611     * logical address as new device info's.
612     *
613     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
614     *
615     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
616     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
617     *         that has the same logical address as new one has.
618     */
619    @ServiceThreadOnly
620    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
621        assertRunOnServiceThread();
622        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
623        if (oldDeviceInfo != null) {
624            removeDeviceInfo(deviceInfo.getLogicalAddress());
625        }
626        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
627        updateSafeDeviceInfoList();
628        return oldDeviceInfo;
629    }
630
631    /**
632     * Remove a device info corresponding to the given {@code logicalAddress}.
633     * It returns removed {@link HdmiCecDeviceInfo} if exists.
634     *
635     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
636     *
637     * @param logicalAddress logical address of device to be removed
638     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
639     */
640    @ServiceThreadOnly
641    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
642        assertRunOnServiceThread();
643        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
644        if (deviceInfo != null) {
645            mDeviceInfos.remove(logicalAddress);
646        }
647        updateSafeDeviceInfoList();
648        return deviceInfo;
649    }
650
651    /**
652     * Return a list of all {@link HdmiCecDeviceInfo}.
653     *
654     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
655     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
656     */
657    @ServiceThreadOnly
658    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
659        assertRunOnServiceThread();
660        if (includelLocalDevice) {
661            return HdmiUtils.sparseArrayToList(mDeviceInfos);
662        } else {
663            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
664            for (int i = 0; i < mDeviceInfos.size(); ++i) {
665                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
666                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
667                    infoList.add(info);
668                }
669            }
670            return infoList;
671        }
672    }
673
674    /**
675     * Return a list of  {@link HdmiCecDeviceInfo}.
676     *
677     * @param includeLocalDevice whether to include local device in result.
678     */
679    List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) {
680        synchronized (mLock) {
681            if (includeLocalDevice) {
682                return mSafeAllDeviceInfos;
683            } else {
684                return mSafeExternalDeviceInfos;
685            }
686        }
687    }
688
689    @ServiceThreadOnly
690    private void updateSafeDeviceInfoList() {
691        assertRunOnServiceThread();
692        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
693        List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false);
694        synchronized (mLock) {
695            mSafeAllDeviceInfos = copiedDevices;
696            mSafeExternalDeviceInfos = externalDeviceInfos;
697        }
698    }
699
700    @ServiceThreadOnly
701    private boolean isLocalDeviceAddress(int address) {
702        assertRunOnServiceThread();
703        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
704            if (device.isAddressOf(address)) {
705                return true;
706            }
707        }
708        return false;
709    }
710
711    @ServiceThreadOnly
712    HdmiCecDeviceInfo getAvrDeviceInfo() {
713        assertRunOnServiceThread();
714        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
715    }
716
717    /**
718     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
719     *
720     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
721     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
722     *
723     * @param logicalAddress logical address to be retrieved
724     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
725     *         Returns null if no logical address matched
726     */
727    @ServiceThreadOnly
728    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
729        assertRunOnServiceThread();
730        return mDeviceInfos.get(logicalAddress);
731    }
732
733    boolean hasSystemAudioDevice() {
734        return getSafeAvrDeviceInfo() != null;
735    }
736
737    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
738        return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
739    }
740
741    /**
742     * Thread safe version of {@link #getDeviceInfo(int)}.
743     *
744     * @param logicalAddress logical address to be retrieved
745     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
746     *         Returns null if no logical address matched
747     */
748    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
749        synchronized (mLock) {
750            return mSafeAllDeviceInfos.get(logicalAddress);
751        }
752    }
753
754    /**
755     * Called when a device is newly added or a new device is detected.
756     *
757     * @param info device info of a new device.
758     */
759    @ServiceThreadOnly
760    final void addCecDevice(HdmiCecDeviceInfo info) {
761        assertRunOnServiceThread();
762        addDeviceInfo(info);
763        if (info.getLogicalAddress() == mAddress) {
764            // The addition of TV device itself should not be notified.
765            return;
766        }
767        mService.invokeDeviceEventListeners(info, true);
768    }
769
770    /**
771     * Called when a device is removed or removal of device is detected.
772     *
773     * @param address a logical address of a device to be removed
774     */
775    @ServiceThreadOnly
776    final void removeCecDevice(int address) {
777        assertRunOnServiceThread();
778        HdmiCecDeviceInfo info = removeDeviceInfo(address);
779        mCecMessageCache.flushMessagesFrom(address);
780        mService.invokeDeviceEventListeners(info, false);
781    }
782
783    /**
784     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
785     * the given routing path. CEC devices use routing path for its physical address to
786     * describe the hierarchy of the devices in the network.
787     *
788     * @param path routing path or physical address
789     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
790     */
791    @ServiceThreadOnly
792    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
793        assertRunOnServiceThread();
794        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
795            if (info.getPhysicalAddress() == path) {
796                return info;
797            }
798        }
799        return null;
800    }
801
802    /**
803     * Whether a device of the specified physical address and logical address exists
804     * in a device info list. However, both are minimal condition and it could
805     * be different device from the original one.
806     *
807     * @param physicalAddress physical address of a device to be searched
808     * @param logicalAddress logical address of a device to be searched
809     * @return true if exist; otherwise false
810     */
811    @ServiceThreadOnly
812    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
813        assertRunOnServiceThread();
814        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
815        if (device == null) {
816            return false;
817        }
818        return device.getPhysicalAddress() == physicalAddress;
819    }
820
821    @Override
822    @ServiceThreadOnly
823    void onHotplug(int portNo, boolean connected) {
824        assertRunOnServiceThread();
825
826        // Tv device will have permanent HotplugDetectionAction.
827        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
828        if (!hotplugActions.isEmpty()) {
829            // Note that hotplug action is single action running on a machine.
830            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
831            hotplugActions.get(0).pollAllDevicesNow();
832        }
833    }
834}
835