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