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