HdmiCecLocalDeviceTv.java revision ca5be9a8fbc91daece39c851ca3f09d60b24b870
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    // # Seq 25
500    void setSystemAudioMode(boolean on) {
501        synchronized (mLock) {
502            if (on != mSystemAudioMode) {
503                mSystemAudioMode = on;
504                // TODO: Need to set the preference for SystemAudioMode.
505                mService.announceSystemAudioModeChange(on);
506            }
507        }
508    }
509
510    boolean getSystemAudioMode() {
511        synchronized (mLock) {
512            return mSystemAudioMode;
513        }
514    }
515
516    /**
517     * Change ARC status into the given {@code enabled} status.
518     *
519     * @return {@code true} if ARC was in "Enabled" status
520     */
521    @ServiceThreadOnly
522    boolean setArcStatus(boolean enabled) {
523        assertRunOnServiceThread();
524        boolean oldStatus = mArcEstablished;
525        // 1. Enable/disable ARC circuit.
526        mService.setAudioReturnChannel(enabled);
527        // 2. Notify arc status to audio service.
528        notifyArcStatusToAudioService(enabled);
529        // 3. Update arc status;
530        mArcEstablished = enabled;
531        return oldStatus;
532    }
533
534    private void notifyArcStatusToAudioService(boolean enabled) {
535        // Note that we don't set any name to ARC.
536        mService.getAudioManager().setWiredDeviceConnectionState(
537                AudioSystem.DEVICE_OUT_HDMI_ARC,
538                enabled ? 1 : 0, "");
539    }
540
541    /**
542     * Returns whether ARC is enabled or not.
543     */
544    @ServiceThreadOnly
545    boolean isArcEstabilished() {
546        assertRunOnServiceThread();
547        return mArcFeatureEnabled && mArcEstablished;
548    }
549
550    @ServiceThreadOnly
551    void changeArcFeatureEnabled(boolean enabled) {
552        assertRunOnServiceThread();
553
554        if (mArcFeatureEnabled != enabled) {
555            if (enabled) {
556                if (!mArcEstablished) {
557                    startArcAction(true);
558                }
559            } else {
560                if (mArcEstablished) {
561                    startArcAction(false);
562                }
563            }
564            mArcFeatureEnabled = enabled;
565        }
566    }
567
568    @ServiceThreadOnly
569    boolean isArcFeatureEnabled() {
570        assertRunOnServiceThread();
571        return mArcFeatureEnabled;
572    }
573
574    @ServiceThreadOnly
575    private void startArcAction(boolean enabled) {
576        assertRunOnServiceThread();
577        HdmiCecDeviceInfo info = getAvrDeviceInfo();
578        if (info == null) {
579            return;
580        }
581        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
582            return;
583        }
584
585        // Terminate opposite action and start action if not exist.
586        if (enabled) {
587            removeAction(RequestArcTerminationAction.class);
588            if (!hasAction(RequestArcInitiationAction.class)) {
589                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
590            }
591        } else {
592            removeAction(RequestArcInitiationAction.class);
593            if (!hasAction(RequestArcTerminationAction.class)) {
594                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
595            }
596        }
597    }
598
599    void setAudioStatus(boolean mute, int volume) {
600        synchronized (mLock) {
601            mSystemAudioMute = mute;
602            mSystemAudioVolume = volume;
603            // TODO: pass volume to service (audio service) after scale it to local volume level.
604            mService.setAudioStatus(mute, volume);
605        }
606    }
607
608    @ServiceThreadOnly
609    void changeVolume(int curVolume, int delta, int maxVolume) {
610        assertRunOnServiceThread();
611        if (delta == 0 || !isSystemAudioOn()) {
612            return;
613        }
614
615        int targetVolume = curVolume + delta;
616        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
617        synchronized (mLock) {
618            // If new volume is the same as current system audio volume, just ignore it.
619            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
620            if (cecVolume == mSystemAudioVolume) {
621                // Update tv volume with system volume value.
622                mService.setAudioStatus(false,
623                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
624                return;
625            }
626        }
627
628        // Remove existing volume action.
629        removeAction(VolumeControlAction.class);
630
631        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
632        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
633                cecVolume, delta > 0));
634    }
635
636    @ServiceThreadOnly
637    void changeMute(boolean mute) {
638        assertRunOnServiceThread();
639        if (!isSystemAudioOn()) {
640            return;
641        }
642
643        // Remove existing volume action.
644        removeAction(VolumeControlAction.class);
645        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
646        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
647    }
648
649    private boolean isSystemAudioOn() {
650        if (getAvrDeviceInfo() == null) {
651            return false;
652        }
653
654        synchronized (mLock) {
655            return mSystemAudioMode;
656        }
657    }
658
659    @Override
660    @ServiceThreadOnly
661    protected boolean handleInitiateArc(HdmiCecMessage message) {
662        assertRunOnServiceThread();
663        // In case where <Initiate Arc> is started by <Request ARC Initiation>
664        // need to clean up RequestArcInitiationAction.
665        removeAction(RequestArcInitiationAction.class);
666        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
667                message.getSource(), true);
668        addAndStartAction(action);
669        return true;
670    }
671
672    @Override
673    @ServiceThreadOnly
674    protected boolean handleTerminateArc(HdmiCecMessage message) {
675        assertRunOnServiceThread();
676        // In case where <Terminate Arc> is started by <Request ARC Termination>
677        // need to clean up RequestArcInitiationAction.
678        // TODO: check conditions of power status by calling is_connected api
679        // to be added soon.
680        removeAction(RequestArcTerminationAction.class);
681        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
682                message.getSource(), false);
683        addAndStartAction(action);
684        return true;
685    }
686
687    @Override
688    @ServiceThreadOnly
689    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
690        assertRunOnServiceThread();
691        if (!isMessageForSystemAudio(message)) {
692            return false;
693        }
694        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
695                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
696        addAndStartAction(action);
697        return true;
698    }
699
700    @Override
701    @ServiceThreadOnly
702    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
703        assertRunOnServiceThread();
704        if (!isMessageForSystemAudio(message)) {
705            return false;
706        }
707        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
708        return true;
709    }
710
711    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
712        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
713                || message.getDestination() != HdmiCec.ADDR_TV
714                || getAvrDeviceInfo() == null) {
715            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
716            return false;
717        }
718        return true;
719    }
720
721    /**
722     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
723     * logical address as new device info's.
724     *
725     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
726     *
727     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
728     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
729     *         that has the same logical address as new one has.
730     */
731    @ServiceThreadOnly
732    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
733        assertRunOnServiceThread();
734        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
735        if (oldDeviceInfo != null) {
736            removeDeviceInfo(deviceInfo.getLogicalAddress());
737        }
738        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
739        updateSafeDeviceInfoList();
740        return oldDeviceInfo;
741    }
742
743    /**
744     * Remove a device info corresponding to the given {@code logicalAddress}.
745     * It returns removed {@link HdmiCecDeviceInfo} if exists.
746     *
747     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
748     *
749     * @param logicalAddress logical address of device to be removed
750     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
751     */
752    @ServiceThreadOnly
753    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
754        assertRunOnServiceThread();
755        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
756        if (deviceInfo != null) {
757            mDeviceInfos.remove(logicalAddress);
758        }
759        updateSafeDeviceInfoList();
760        return deviceInfo;
761    }
762
763    /**
764     * Return a list of all {@link HdmiCecDeviceInfo}.
765     *
766     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
767     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
768     */
769    @ServiceThreadOnly
770    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
771        assertRunOnServiceThread();
772        if (includelLocalDevice) {
773            return HdmiUtils.sparseArrayToList(mDeviceInfos);
774        } else {
775            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
776            for (int i = 0; i < mDeviceInfos.size(); ++i) {
777                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
778                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
779                    infoList.add(info);
780                }
781            }
782            return infoList;
783        }
784    }
785
786    /**
787     * Return a list of  {@link HdmiCecDeviceInfo}.
788     *
789     * @param includeLocalDevice whether to include local device in result.
790     */
791    List<HdmiCecDeviceInfo> getSafeDeviceInfoList(boolean includeLocalDevice) {
792        synchronized (mLock) {
793            if (includeLocalDevice) {
794                return mSafeAllDeviceInfos;
795            } else {
796                return mSafeExternalDeviceInfos;
797            }
798        }
799    }
800
801    @ServiceThreadOnly
802    private void updateSafeDeviceInfoList() {
803        assertRunOnServiceThread();
804        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
805        List<HdmiCecDeviceInfo> externalDeviceInfos = getDeviceInfoList(false);
806        synchronized (mLock) {
807            mSafeAllDeviceInfos = copiedDevices;
808            mSafeExternalDeviceInfos = externalDeviceInfos;
809        }
810    }
811
812    @ServiceThreadOnly
813    private boolean isLocalDeviceAddress(int address) {
814        assertRunOnServiceThread();
815        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
816            if (device.isAddressOf(address)) {
817                return true;
818            }
819        }
820        return false;
821    }
822
823    @ServiceThreadOnly
824    HdmiCecDeviceInfo getAvrDeviceInfo() {
825        assertRunOnServiceThread();
826        return getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
827    }
828
829    /**
830     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
831     *
832     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
833     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
834     *
835     * @param logicalAddress logical address to be retrieved
836     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
837     *         Returns null if no logical address matched
838     */
839    @ServiceThreadOnly
840    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
841        assertRunOnServiceThread();
842        return mDeviceInfos.get(logicalAddress);
843    }
844
845    boolean hasSystemAudioDevice() {
846        return getSafeAvrDeviceInfo() != null;
847    }
848
849    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
850        return getSafeDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
851    }
852
853    /**
854     * Thread safe version of {@link #getDeviceInfo(int)}.
855     *
856     * @param logicalAddress logical address to be retrieved
857     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
858     *         Returns null if no logical address matched
859     */
860    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
861        synchronized (mLock) {
862            return mSafeAllDeviceInfos.get(logicalAddress);
863        }
864    }
865
866    /**
867     * Called when a device is newly added or a new device is detected.
868     *
869     * @param info device info of a new device.
870     */
871    @ServiceThreadOnly
872    final void addCecDevice(HdmiCecDeviceInfo info) {
873        assertRunOnServiceThread();
874        addDeviceInfo(info);
875        if (info.getLogicalAddress() == mAddress) {
876            // The addition of TV device itself should not be notified.
877            return;
878        }
879        mService.invokeDeviceEventListeners(info, true);
880    }
881
882    /**
883     * Called when a device is removed or removal of device is detected.
884     *
885     * @param address a logical address of a device to be removed
886     */
887    @ServiceThreadOnly
888    final void removeCecDevice(int address) {
889        assertRunOnServiceThread();
890        HdmiCecDeviceInfo info = removeDeviceInfo(address);
891        handleRemoveActiveRoutingPath(info.getPhysicalAddress());
892        mCecMessageCache.flushMessagesFrom(address);
893        mService.invokeDeviceEventListeners(info, false);
894    }
895
896    private void handleRemoveActiveRoutingPath(int path) {
897        // Seq #23
898        if (isTailOfActivePath(path, getActivePath())) {
899            removeAction(RoutingControlAction.class);
900            int newPath = mService.portIdToPath(getActivePortId());
901            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
902                    mAddress, getActivePath(), newPath));
903            addAndStartAction(new RoutingControlAction(this, getActivePortId(), null));
904        }
905    }
906
907    @ServiceThreadOnly
908    void routingAtEnableTime() {
909        assertRunOnServiceThread();
910        // Seq #24
911        if (getActivePortId() != HdmiConstants.INVALID_PORT_ID) {
912            // TODO: Check if TV was not powered on due to <Text/Image View On>,
913            //       TV is not in Preset Installation mode, not in initial setup mode, not
914            //       in Software updating mode, not in service mode, for following actions.
915            removeAction(RoutingControlAction.class);
916            int newPath = mService.portIdToPath(getActivePortId());
917            mService.sendCecCommand(
918                    HdmiCecMessageBuilder.buildRoutingChange(mAddress, getActivePath(), newPath));
919            addAndStartAction(new RoutingControlAction(this, getActivePortId(), null));
920        } else {
921            int activePath = mService.getPhysicalAddress();
922            setActivePath(activePath);
923            // TODO: Do following only when TV was not powered on due to <Text/Image View On>.
924            mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, activePath));
925        }
926    }
927
928    /**
929     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
930     * the given routing path. CEC devices use routing path for its physical address to
931     * describe the hierarchy of the devices in the network.
932     *
933     * @param path routing path or physical address
934     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
935     */
936    @ServiceThreadOnly
937    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
938        assertRunOnServiceThread();
939        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
940            if (info.getPhysicalAddress() == path) {
941                return info;
942            }
943        }
944        return null;
945    }
946
947    /**
948     * Whether a device of the specified physical address and logical address exists
949     * in a device info list. However, both are minimal condition and it could
950     * be different device from the original one.
951     *
952     * @param logicalAddress logical address of a device to be searched
953     * @param physicalAddress physical address of a device to be searched
954     * @return true if exist; otherwise false
955     */
956    @ServiceThreadOnly
957    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
958        assertRunOnServiceThread();
959        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
960        if (device == null) {
961            return false;
962        }
963        return device.getPhysicalAddress() == physicalAddress;
964    }
965
966    @Override
967    @ServiceThreadOnly
968    void onHotplug(int portId, boolean connected) {
969        assertRunOnServiceThread();
970
971        // Tv device will have permanent HotplugDetectionAction.
972        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
973        if (!hotplugActions.isEmpty()) {
974            // Note that hotplug action is single action running on a machine.
975            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
976            hotplugActions.get(0).pollAllDevicesNow();
977        }
978    }
979}
980