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