HdmiCecLocalDeviceTv.java revision 8fa36b110be29d92a9aba070fa4666eefb14b584
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                        }
407                    }
408                });
409        addAndStartAction(action);
410    }
411
412    // Clear all device info.
413    @ServiceThreadOnly
414    private void clearDeviceInfoList() {
415        assertRunOnServiceThread();
416        mDeviceInfos.clear();
417        updateSafeDeviceInfoList();
418    }
419
420    @ServiceThreadOnly
421    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
422        assertRunOnServiceThread();
423        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
424        if (avr == null) {
425            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
426            return;
427        }
428
429        addAndStartAction(
430                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
431    }
432
433    void setSystemAudioMode(boolean on) {
434        synchronized (mLock) {
435            if (on != mSystemAudioMode) {
436                mSystemAudioMode = on;
437                // TODO: Need to set the preference for SystemAudioMode.
438                mService.announceSystemAudioModeChange(on);
439            }
440        }
441    }
442
443    boolean getSystemAudioMode() {
444        synchronized (mLock) {
445            return mSystemAudioMode;
446        }
447    }
448
449    /**
450     * Change ARC status into the given {@code enabled} status.
451     *
452     * @return {@code true} if ARC was in "Enabled" status
453     */
454    boolean setArcStatus(boolean enabled) {
455        synchronized (mLock) {
456            boolean oldStatus = mArcStatusEnabled;
457            // 1. Enable/disable ARC circuit.
458            mService.setAudioReturnChannel(enabled);
459            // 2. Notify arc status to audio service.
460            notifyArcStatusToAudioService(enabled);
461            // 3. Update arc status;
462            mArcStatusEnabled = enabled;
463            return oldStatus;
464        }
465    }
466
467    private void notifyArcStatusToAudioService(boolean enabled) {
468        // Note that we don't set any name to ARC.
469        mService.getAudioManager().setWiredDeviceConnectionState(
470                AudioSystem.DEVICE_OUT_HDMI_ARC,
471                enabled ? 1 : 0, "");
472    }
473
474    /**
475     * Returns whether ARC is enabled or not.
476     */
477    boolean getArcStatus() {
478        synchronized (mLock) {
479            return mArcStatusEnabled;
480        }
481    }
482
483    void setAudioStatus(boolean mute, int volume) {
484        synchronized (mLock) {
485            mSystemAudioMute = mute;
486            mSystemAudioVolume = volume;
487            // TODO: pass volume to service (audio service) after scale it to local volume level.
488            mService.setAudioStatus(mute, volume);
489        }
490    }
491
492    @ServiceThreadOnly
493    void changeVolume(int curVolume, int delta, int maxVolume) {
494        assertRunOnServiceThread();
495        if (delta == 0 || !isSystemAudioOn()) {
496            return;
497        }
498
499        int targetVolume = curVolume + delta;
500        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
501        synchronized (mLock) {
502            // If new volume is the same as current system audio volume, just ignore it.
503            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
504            if (cecVolume == mSystemAudioVolume) {
505                // Update tv volume with system volume value.
506                mService.setAudioStatus(false,
507                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
508                return;
509            }
510        }
511
512        // Remove existing volume action.
513        removeAction(VolumeControlAction.class);
514
515        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
516        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
517                cecVolume, delta > 0));
518    }
519
520    @ServiceThreadOnly
521    void changeMute(boolean mute) {
522        assertRunOnServiceThread();
523        if (!isSystemAudioOn()) {
524            return;
525        }
526
527        // Remove existing volume action.
528        removeAction(VolumeControlAction.class);
529        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
530        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
531    }
532
533    private boolean isSystemAudioOn() {
534        if (getAvrDeviceInfo() == null) {
535            return false;
536        }
537
538        synchronized (mLock) {
539            return mSystemAudioMode;
540        }
541    }
542
543    @Override
544    @ServiceThreadOnly
545    protected boolean handleInitiateArc(HdmiCecMessage message) {
546        assertRunOnServiceThread();
547        // In case where <Initiate Arc> is started by <Request ARC Initiation>
548        // need to clean up RequestArcInitiationAction.
549        removeAction(RequestArcInitiationAction.class);
550        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
551                message.getSource(), true);
552        addAndStartAction(action);
553        return true;
554    }
555
556    @Override
557    @ServiceThreadOnly
558    protected boolean handleTerminateArc(HdmiCecMessage message) {
559        assertRunOnServiceThread();
560        // In case where <Terminate Arc> is started by <Request ARC Termination>
561        // need to clean up RequestArcInitiationAction.
562        // TODO: check conditions of power status by calling is_connected api
563        // to be added soon.
564        removeAction(RequestArcTerminationAction.class);
565        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
566                message.getSource(), false);
567        addAndStartAction(action);
568        return true;
569    }
570
571    @Override
572    @ServiceThreadOnly
573    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
574        assertRunOnServiceThread();
575        if (!isMessageForSystemAudio(message)) {
576            return false;
577        }
578        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
579                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
580        addAndStartAction(action);
581        return true;
582    }
583
584    @Override
585    @ServiceThreadOnly
586    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
587        assertRunOnServiceThread();
588        if (!isMessageForSystemAudio(message)) {
589            return false;
590        }
591        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
592        return true;
593    }
594
595    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
596        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
597                || message.getDestination() != HdmiCec.ADDR_TV
598                || getAvrDeviceInfo() == null) {
599            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
600            return false;
601        }
602        return true;
603    }
604
605    /**
606     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
607     * logical address as new device info's.
608     *
609     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
610     *
611     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
612     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
613     *         that has the same logical address as new one has.
614     */
615    @ServiceThreadOnly
616    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
617        assertRunOnServiceThread();
618        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
619        if (oldDeviceInfo != null) {
620            removeDeviceInfo(deviceInfo.getLogicalAddress());
621        }
622        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
623        updateSafeDeviceInfoList();
624        return oldDeviceInfo;
625    }
626
627    /**
628     * Remove a device info corresponding to the given {@code logicalAddress}.
629     * It returns removed {@link HdmiCecDeviceInfo} if exists.
630     *
631     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
632     *
633     * @param logicalAddress logical address of device to be removed
634     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
635     */
636    @ServiceThreadOnly
637    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
638        assertRunOnServiceThread();
639        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
640        if (deviceInfo != null) {
641            mDeviceInfos.remove(logicalAddress);
642        }
643        updateSafeDeviceInfoList();
644        return deviceInfo;
645    }
646
647    /**
648     * Return a list of all {@link HdmiCecDeviceInfo}.
649     *
650     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
651     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
652     */
653    @ServiceThreadOnly
654    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
655        assertRunOnServiceThread();
656        if (includelLocalDevice) {
657            return HdmiUtils.sparseArrayToList(mDeviceInfos);
658        } else {
659            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
660            for (int i = 0; i < mDeviceInfos.size(); ++i) {
661                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
662                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
663                    infoList.add(info);
664                }
665            }
666            return infoList;
667        }
668    }
669
670    /**
671     * Return a list of  {@link HdmiCecDeviceInfo}.
672     *
673     * @param includeLocalDevice whether to include local device in result.
674     */
675    List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) {
676        synchronized (mLock) {
677            if (includeLocalDevice) {
678                return mSafeAllDeviceInfos;
679            } else {
680                return mSafeExternalDeviceInfos;
681            }
682        }
683    }
684
685    @ServiceThreadOnly
686    private void updateSafeDeviceInfoList() {
687        assertRunOnServiceThread();
688        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
689        List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false);
690        synchronized (mLock) {
691            mSafeAllDeviceInfos = copiedDevices;
692            mSafeExternalDeviceInfos = externalDeviceInfos;
693        }
694    }
695
696    @ServiceThreadOnly
697    private boolean isLocalDeviceAddress(int address) {
698        assertRunOnServiceThread();
699        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
700            if (device.isAddressOf(address)) {
701                return true;
702            }
703        }
704        return false;
705    }
706
707    @ServiceThreadOnly
708    HdmiCecDeviceInfo getAvrDeviceInfo() {
709        assertRunOnServiceThread();
710        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
711    }
712
713    /**
714     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
715     *
716     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
717     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
718     *
719     * @param logicalAddress logical address to be retrieved
720     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
721     *         Returns null if no logical address matched
722     */
723    @ServiceThreadOnly
724    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
725        assertRunOnServiceThread();
726        return mDeviceInfos.get(logicalAddress);
727    }
728
729    boolean hasSystemAudioDevice() {
730        return getSafeAvrDeviceInfo() != null;
731    }
732
733    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
734        return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
735    }
736
737    /**
738     * Thread safe version of {@link #getDeviceInfo(int)}.
739     *
740     * @param logicalAddress logical address to be retrieved
741     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
742     *         Returns null if no logical address matched
743     */
744    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
745        synchronized (mLock) {
746            return mSafeAllDeviceInfos.get(logicalAddress);
747        }
748    }
749
750    /**
751     * Called when a device is newly added or a new device is detected.
752     *
753     * @param info device info of a new device.
754     */
755    @ServiceThreadOnly
756    final void addCecDevice(HdmiCecDeviceInfo info) {
757        assertRunOnServiceThread();
758        addDeviceInfo(info);
759        if (info.getLogicalAddress() == mAddress) {
760            // The addition of TV device itself should not be notified.
761            return;
762        }
763        mService.invokeDeviceEventListeners(info, true);
764    }
765
766    /**
767     * Called when a device is removed or removal of device is detected.
768     *
769     * @param address a logical address of a device to be removed
770     */
771    @ServiceThreadOnly
772    final void removeCecDevice(int address) {
773        assertRunOnServiceThread();
774        HdmiCecDeviceInfo info = removeDeviceInfo(address);
775        mCecMessageCache.flushMessagesFrom(address);
776        mService.invokeDeviceEventListeners(info, false);
777    }
778
779    /**
780     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
781     * the given routing path. CEC devices use routing path for its physical address to
782     * describe the hierarchy of the devices in the network.
783     *
784     * @param path routing path or physical address
785     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
786     */
787    @ServiceThreadOnly
788    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
789        assertRunOnServiceThread();
790        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
791            if (info.getPhysicalAddress() == path) {
792                return info;
793            }
794        }
795        return null;
796    }
797
798    /**
799     * Whether a device of the specified physical address and logical address exists
800     * in a device info list. However, both are minimal condition and it could
801     * be different device from the original one.
802     *
803     * @param physicalAddress physical address of a device to be searched
804     * @param logicalAddress logical address of a device to be searched
805     * @return true if exist; otherwise false
806     */
807    @ServiceThreadOnly
808    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
809        assertRunOnServiceThread();
810        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
811        if (device == null) {
812            return false;
813        }
814        return device.getPhysicalAddress() == physicalAddress;
815    }
816
817    @Override
818    @ServiceThreadOnly
819    void onHotplug(int portNo, boolean connected) {
820        assertRunOnServiceThread();
821
822        // Tv device will have permanent HotplugDetectionAction.
823        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
824        if (!hotplugActions.isEmpty()) {
825            // Note that hotplug action is single action running on a machine.
826            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
827            hotplugActions.get(0).pollAllDevicesNow();
828        }
829    }
830}
831