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