HdmiCecLocalDeviceTv.java revision 4fc1d105fc279bf7df6c876e160672866bdad8e7
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 key press event
228     */
229    @Override
230    @ServiceThreadOnly
231    protected void sendKeyEvent(int keyCode, boolean isPressed) {
232        assertRunOnServiceThread();
233        List<SendKeyAction> action = getActions(SendKeyAction.class);
234        if (!action.isEmpty()) {
235            action.get(0).processKeyEvent(keyCode, isPressed);
236        } else {
237            if (isPressed) {
238                addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode));
239            } else {
240                Slog.w(TAG, "Discard key release event");
241            }
242        }
243    }
244
245    private static void invokeCallback(IHdmiControlCallback callback, int result) {
246        if (callback == null) {
247            return;
248        }
249        try {
250            callback.onComplete(result);
251        } catch (RemoteException e) {
252            Slog.e(TAG, "Invoking callback failed:" + e);
253        }
254    }
255
256    @Override
257    @ServiceThreadOnly
258    protected boolean handleActiveSource(HdmiCecMessage message) {
259        assertRunOnServiceThread();
260        int address = message.getSource();
261        int path = HdmiUtils.twoBytesToInt(message.getParams());
262        if (getDeviceInfo(address) == null) {
263            handleNewDeviceAtTheTailOfActivePath(path);
264        } else {
265            ActiveSourceHandler.create(this, null).process(address, path);
266        }
267        return true;
268    }
269
270    @Override
271    @ServiceThreadOnly
272    protected boolean handleInactiveSource(HdmiCecMessage message) {
273        assertRunOnServiceThread();
274        // Seq #10
275
276        // Ignore <Inactive Source> from non-active source device.
277        if (getActiveSource() != message.getSource()) {
278            return true;
279        }
280        if (isProhibitMode()) {
281            return true;
282        }
283        int portId = getPrevPortId();
284        if (portId != Constants.INVALID_PORT_ID) {
285            // TODO: Do this only if TV is not showing multiview like PIP/PAP.
286
287            HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource());
288            if (inactiveSource == null) {
289                return true;
290            }
291            if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) {
292                return true;
293            }
294            // TODO: Switch the TV freeze mode off
295
296            setActivePortId(portId);
297            doManualPortSwitching(portId, null);
298            setPrevPortId(Constants.INVALID_PORT_ID);
299        }
300        return true;
301    }
302
303    @Override
304    @ServiceThreadOnly
305    protected boolean handleRequestActiveSource(HdmiCecMessage message) {
306        assertRunOnServiceThread();
307        // Seq #19
308        if (mAddress == getActiveSource()) {
309            mService.sendCecCommand(
310                    HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath()));
311        }
312        return true;
313    }
314
315    @Override
316    @ServiceThreadOnly
317    protected boolean handleGetMenuLanguage(HdmiCecMessage message) {
318        assertRunOnServiceThread();
319        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
320                mAddress, Locale.getDefault().getISO3Language());
321        // TODO: figure out how to handle failed to get language code.
322        if (command != null) {
323            mService.sendCecCommand(command);
324        } else {
325            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
326        }
327        return true;
328    }
329
330    @Override
331    @ServiceThreadOnly
332    protected boolean handleReportPhysicalAddress(HdmiCecMessage message) {
333        assertRunOnServiceThread();
334        // Ignore if [Device Discovery Action] is going on.
335        if (hasAction(DeviceDiscoveryAction.class)) {
336            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
337                    + "because Device Discovery Action is on-going:" + message);
338            return true;
339        }
340
341        int path = HdmiUtils.twoBytesToInt(message.getParams());
342        int address = message.getSource();
343        if (!isInDeviceList(path, address)) {
344            handleNewDeviceAtTheTailOfActivePath(path);
345        }
346        addAndStartAction(new NewDeviceAction(this, address, path));
347        return true;
348    }
349
350    private void handleNewDeviceAtTheTailOfActivePath(int path) {
351        // Seq #22
352        if (isTailOfActivePath(path, getActivePath())) {
353            removeAction(RoutingControlAction.class);
354            int newPath = mService.portIdToPath(getActivePortId());
355            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
356                    mAddress, getActivePath(), newPath));
357            addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null));
358        }
359    }
360
361    /**
362     * Whether the given path is located in the tail of current active path.
363     *
364     * @param path to be tested
365     * @param activePath current active path
366     * @return true if the given path is located in the tail of current active path; otherwise,
367     *         false
368     */
369    static boolean isTailOfActivePath(int path, int activePath) {
370        // If active routing path is internal source, return false.
371        if (activePath == 0) {
372            return false;
373        }
374        for (int i = 12; i >= 0; i -= 4) {
375            int curActivePath = (activePath >> i) & 0xF;
376            if (curActivePath == 0) {
377                return true;
378            } else {
379                int curPath = (path >> i) & 0xF;
380                if (curPath != curActivePath) {
381                    return false;
382                }
383            }
384        }
385        return false;
386    }
387
388    @Override
389    @ServiceThreadOnly
390    protected boolean handleRoutingChange(HdmiCecMessage message) {
391        assertRunOnServiceThread();
392        // Seq #21
393        byte[] params = message.getParams();
394        int currentPath = HdmiUtils.twoBytesToInt(params);
395        if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) {
396            int newPath = HdmiUtils.twoBytesToInt(params, 2);
397            setActivePath(newPath);
398            removeAction(RoutingControlAction.class);
399            addAndStartAction(new RoutingControlAction(this, newPath, true, null));
400        }
401        return true;
402    }
403
404    @Override
405    @ServiceThreadOnly
406    protected boolean handleReportAudioStatus(HdmiCecMessage message) {
407        assertRunOnServiceThread();
408
409        byte params[] = message.getParams();
410        int mute = params[0] & 0x80;
411        int volume = params[0] & 0x7F;
412        setAudioStatus(mute == 0x80, volume);
413        return true;
414    }
415
416    @Override
417    @ServiceThreadOnly
418    protected boolean handleTextViewOn(HdmiCecMessage message) {
419        assertRunOnServiceThread();
420        if (mService.isPowerStandbyOrTransient()) {
421            mService.wakeUp();
422        }
423        // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel
424        //       that represents the source device.
425        return true;
426    }
427
428    @Override
429    @ServiceThreadOnly
430    protected boolean handleImageViewOn(HdmiCecMessage message) {
431        assertRunOnServiceThread();
432        // Currently, it's the same as <Text View On>.
433        return handleTextViewOn(message);
434    }
435
436    @Override
437    @ServiceThreadOnly
438    protected boolean handleSetOsdName(HdmiCecMessage message) {
439        int source = message.getSource();
440        HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source);
441        // If the device is not in device list, ignore it.
442        if (deviceInfo == null) {
443            Slog.e(TAG, "No source device info for <Set Osd Name>." + message);
444            return true;
445        }
446        String osdName = null;
447        try {
448            osdName = new String(message.getParams(), "US-ASCII");
449        } catch (UnsupportedEncodingException e) {
450            Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e);
451            return true;
452        }
453
454        if (deviceInfo.getDisplayName().equals(osdName)) {
455            Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message);
456            return true;
457        }
458
459        addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(),
460                deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(),
461                deviceInfo.getVendorId(), osdName));
462        return true;
463    }
464
465    @ServiceThreadOnly
466    private void launchDeviceDiscovery() {
467        assertRunOnServiceThread();
468        clearDeviceInfoList();
469        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
470                new DeviceDiscoveryCallback() {
471                    @Override
472                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
473                        for (HdmiCecDeviceInfo info : deviceInfos) {
474                            addCecDevice(info);
475                        }
476
477                        // Since we removed all devices when it's start and
478                        // device discovery action does not poll local devices,
479                        // we should put device info of local device manually here
480                        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
481                            addCecDevice(device.getDeviceInfo());
482                        }
483
484                        addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
485
486                        // If there is AVR, initiate System Audio Auto initiation action,
487                        // which turns on and off system audio according to last system
488                        // audio setting.
489                        HdmiCecDeviceInfo avrInfo = getAvrDeviceInfo();
490                        if (avrInfo != null) {
491                            addAndStartAction(new SystemAudioAutoInitiationAction(
492                                    HdmiCecLocalDeviceTv.this, avrInfo.getLogicalAddress()));
493                            if (mArcEstablished) {
494                                startArcAction(true);
495                            }
496                        }
497                    }
498                });
499        addAndStartAction(action);
500    }
501
502    // Clear all device info.
503    @ServiceThreadOnly
504    private void clearDeviceInfoList() {
505        assertRunOnServiceThread();
506        mDeviceInfos.clear();
507        updateSafeDeviceInfoList();
508    }
509
510    @ServiceThreadOnly
511    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
512        assertRunOnServiceThread();
513        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
514        if (avr == null) {
515            invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
516            return;
517        }
518
519        addAndStartAction(
520                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
521    }
522
523    // # Seq 25
524    void setSystemAudioMode(boolean on) {
525        synchronized (mLock) {
526            if (on != mSystemAudioMode) {
527                mSystemAudioMode = on;
528                // TODO: Need to set the preference for SystemAudioMode.
529                mService.announceSystemAudioModeChange(on);
530            }
531        }
532    }
533
534    boolean getSystemAudioMode() {
535        synchronized (mLock) {
536            return mSystemAudioMode;
537        }
538    }
539
540    /**
541     * Change ARC status into the given {@code enabled} status.
542     *
543     * @return {@code true} if ARC was in "Enabled" status
544     */
545    @ServiceThreadOnly
546    boolean setArcStatus(boolean enabled) {
547        assertRunOnServiceThread();
548        boolean oldStatus = mArcEstablished;
549        // 1. Enable/disable ARC circuit.
550        mService.setAudioReturnChannel(enabled);
551        // 2. Notify arc status to audio service.
552        notifyArcStatusToAudioService(enabled);
553        // 3. Update arc status;
554        mArcEstablished = enabled;
555        return oldStatus;
556    }
557
558    private void notifyArcStatusToAudioService(boolean enabled) {
559        // Note that we don't set any name to ARC.
560        mService.getAudioManager().setWiredDeviceConnectionState(
561                AudioSystem.DEVICE_OUT_HDMI_ARC,
562                enabled ? 1 : 0, "");
563    }
564
565    /**
566     * Returns whether ARC is enabled or not.
567     */
568    @ServiceThreadOnly
569    boolean isArcEstabilished() {
570        assertRunOnServiceThread();
571        return mArcFeatureEnabled && mArcEstablished;
572    }
573
574    @ServiceThreadOnly
575    void changeArcFeatureEnabled(boolean enabled) {
576        assertRunOnServiceThread();
577
578        if (mArcFeatureEnabled != enabled) {
579            if (enabled) {
580                if (!mArcEstablished) {
581                    startArcAction(true);
582                }
583            } else {
584                if (mArcEstablished) {
585                    startArcAction(false);
586                }
587            }
588            mArcFeatureEnabled = enabled;
589        }
590    }
591
592    @ServiceThreadOnly
593    boolean isArcFeatureEnabled() {
594        assertRunOnServiceThread();
595        return mArcFeatureEnabled;
596    }
597
598    @ServiceThreadOnly
599    private void startArcAction(boolean enabled) {
600        assertRunOnServiceThread();
601        HdmiCecDeviceInfo info = getAvrDeviceInfo();
602        if (info == null) {
603            return;
604        }
605        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
606            return;
607        }
608
609        // Terminate opposite action and start action if not exist.
610        if (enabled) {
611            removeAction(RequestArcTerminationAction.class);
612            if (!hasAction(RequestArcInitiationAction.class)) {
613                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
614            }
615        } else {
616            removeAction(RequestArcInitiationAction.class);
617            if (!hasAction(RequestArcTerminationAction.class)) {
618                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
619            }
620        }
621    }
622
623    void setAudioStatus(boolean mute, int volume) {
624        synchronized (mLock) {
625            mSystemAudioMute = mute;
626            mSystemAudioVolume = volume;
627            // TODO: pass volume to service (audio service) after scale it to local volume level.
628            mService.setAudioStatus(mute, volume);
629        }
630    }
631
632    @ServiceThreadOnly
633    void changeVolume(int curVolume, int delta, int maxVolume) {
634        assertRunOnServiceThread();
635        if (delta == 0 || !isSystemAudioOn()) {
636            return;
637        }
638
639        int targetVolume = curVolume + delta;
640        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
641        synchronized (mLock) {
642            // If new volume is the same as current system audio volume, just ignore it.
643            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
644            if (cecVolume == mSystemAudioVolume) {
645                // Update tv volume with system volume value.
646                mService.setAudioStatus(false,
647                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
648                return;
649            }
650        }
651
652        // Remove existing volume action.
653        removeAction(VolumeControlAction.class);
654
655        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
656        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
657                cecVolume, delta > 0));
658    }
659
660    @ServiceThreadOnly
661    void changeMute(boolean mute) {
662        assertRunOnServiceThread();
663        if (!isSystemAudioOn()) {
664            return;
665        }
666
667        // Remove existing volume action.
668        removeAction(VolumeControlAction.class);
669        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
670        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
671    }
672
673    private boolean isSystemAudioOn() {
674        if (getAvrDeviceInfo() == null) {
675            return false;
676        }
677
678        synchronized (mLock) {
679            return mSystemAudioMode;
680        }
681    }
682
683    @Override
684    @ServiceThreadOnly
685    protected boolean handleInitiateArc(HdmiCecMessage message) {
686        assertRunOnServiceThread();
687        // In case where <Initiate Arc> is started by <Request ARC Initiation>
688        // need to clean up RequestArcInitiationAction.
689        removeAction(RequestArcInitiationAction.class);
690        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
691                message.getSource(), true);
692        addAndStartAction(action);
693        return true;
694    }
695
696    @Override
697    @ServiceThreadOnly
698    protected boolean handleTerminateArc(HdmiCecMessage message) {
699        assertRunOnServiceThread();
700        // In case where <Terminate Arc> is started by <Request ARC Termination>
701        // need to clean up RequestArcInitiationAction.
702        removeAction(RequestArcTerminationAction.class);
703        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
704                message.getSource(), false);
705        addAndStartAction(action);
706        return true;
707    }
708
709    @Override
710    @ServiceThreadOnly
711    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
712        assertRunOnServiceThread();
713        if (!isMessageForSystemAudio(message)) {
714            return false;
715        }
716        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
717                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
718        addAndStartAction(action);
719        return true;
720    }
721
722    @Override
723    @ServiceThreadOnly
724    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
725        assertRunOnServiceThread();
726        if (!isMessageForSystemAudio(message)) {
727            return false;
728        }
729        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
730        return true;
731    }
732
733    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
734        if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM
735                || message.getDestination() != Constants.ADDR_TV
736                || getAvrDeviceInfo() == null) {
737            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
738            return false;
739        }
740        return true;
741    }
742
743    /**
744     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
745     * logical address as new device info's.
746     *
747     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
748     *
749     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
750     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
751     *         that has the same logical address as new one has.
752     */
753    @ServiceThreadOnly
754    private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
755        assertRunOnServiceThread();
756        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
757        if (oldDeviceInfo != null) {
758            removeDeviceInfo(deviceInfo.getLogicalAddress());
759        }
760        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
761        updateSafeDeviceInfoList();
762        return oldDeviceInfo;
763    }
764
765    /**
766     * Remove a device info corresponding to the given {@code logicalAddress}.
767     * It returns removed {@link HdmiCecDeviceInfo} if exists.
768     *
769     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
770     *
771     * @param logicalAddress logical address of device to be removed
772     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
773     */
774    @ServiceThreadOnly
775    private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
776        assertRunOnServiceThread();
777        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
778        if (deviceInfo != null) {
779            mDeviceInfos.remove(logicalAddress);
780        }
781        updateSafeDeviceInfoList();
782        return deviceInfo;
783    }
784
785    /**
786     * Return a list of all {@link HdmiCecDeviceInfo}.
787     *
788     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
789     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
790     */
791    @ServiceThreadOnly
792    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
793        assertRunOnServiceThread();
794        if (includelLocalDevice) {
795            return HdmiUtils.sparseArrayToList(mDeviceInfos);
796        } else {
797            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
798            for (int i = 0; i < mDeviceInfos.size(); ++i) {
799                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
800                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
801                    infoList.add(info);
802                }
803            }
804            return infoList;
805        }
806    }
807
808    /**
809     * Return external input devices.
810     */
811    List<HdmiCecDeviceInfo> getSafeExternalInputs() {
812        synchronized (mLock) {
813            return mSafeExternalInputs;
814        }
815    }
816
817    @ServiceThreadOnly
818    private void updateSafeDeviceInfoList() {
819        assertRunOnServiceThread();
820        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
821        List<HdmiCecDeviceInfo> externalInputs = getInputDevices();
822        synchronized (mLock) {
823            mSafeAllDeviceInfos = copiedDevices;
824            mSafeExternalInputs = externalInputs;
825        }
826    }
827
828    /**
829     * Return a list of external cec input (source) devices.
830     *
831     * <p>Note that this effectively excludes non-source devices like system audio,
832     * secondary TV.
833     */
834    private List<HdmiCecDeviceInfo> getInputDevices() {
835        ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
836        for (int i = 0; i < mDeviceInfos.size(); ++i) {
837            HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
838            if (isLocalDeviceAddress(i)) {
839                continue;
840            }
841            if (info.isSourceType()) {
842                infoList.add(info);
843            }
844        }
845        return infoList;
846    }
847
848    @ServiceThreadOnly
849    private boolean isLocalDeviceAddress(int address) {
850        assertRunOnServiceThread();
851        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
852            if (device.isAddressOf(address)) {
853                return true;
854            }
855        }
856        return false;
857    }
858
859    @ServiceThreadOnly
860    HdmiCecDeviceInfo getAvrDeviceInfo() {
861        assertRunOnServiceThread();
862        return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
863    }
864
865    /**
866     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
867     *
868     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
869     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
870     *
871     * @param logicalAddress logical address to be retrieved
872     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
873     *         Returns null if no logical address matched
874     */
875    @ServiceThreadOnly
876    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
877        assertRunOnServiceThread();
878        return mDeviceInfos.get(logicalAddress);
879    }
880
881    boolean hasSystemAudioDevice() {
882        return getSafeAvrDeviceInfo() != null;
883    }
884
885    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
886        return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
887    }
888
889    /**
890     * Thread safe version of {@link #getDeviceInfo(int)}.
891     *
892     * @param logicalAddress logical address to be retrieved
893     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
894     *         Returns null if no logical address matched
895     */
896    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
897        synchronized (mLock) {
898            return mSafeAllDeviceInfos.get(logicalAddress);
899        }
900    }
901
902    /**
903     * Called when a device is newly added or a new device is detected or
904     * existing device is updated.
905     *
906     * @param info device info of a new device.
907     */
908    @ServiceThreadOnly
909    final void addCecDevice(HdmiCecDeviceInfo info) {
910        assertRunOnServiceThread();
911        addDeviceInfo(info);
912        if (info.getLogicalAddress() == mAddress) {
913            // The addition of TV device itself should not be notified.
914            return;
915        }
916        mService.invokeDeviceEventListeners(info, true);
917    }
918
919    /**
920     * Called when a device is removed or removal of device is detected.
921     *
922     * @param address a logical address of a device to be removed
923     */
924    @ServiceThreadOnly
925    final void removeCecDevice(int address) {
926        assertRunOnServiceThread();
927        HdmiCecDeviceInfo info = removeDeviceInfo(address);
928
929        mCecMessageCache.flushMessagesFrom(address);
930        mService.invokeDeviceEventListeners(info, false);
931    }
932
933    @ServiceThreadOnly
934    void handleRemoveActiveRoutingPath(int path) {
935        assertRunOnServiceThread();
936        // Seq #23
937        if (isTailOfActivePath(path, getActivePath())) {
938            removeAction(RoutingControlAction.class);
939            int newPath = mService.portIdToPath(getActivePortId());
940            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
941                    mAddress, getActivePath(), newPath));
942            addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null));
943        }
944    }
945
946    /**
947     * Launch routing control process.
948     *
949     * @param routingForBootup true if routing control is initiated due to One Touch Play
950     *        or TV power on
951     */
952    @ServiceThreadOnly
953    void launchRoutingControl(boolean routingForBootup) {
954        assertRunOnServiceThread();
955        // Seq #24
956        if (getActivePortId() != Constants.INVALID_PORT_ID) {
957            if (!routingForBootup && !isProhibitMode()) {
958                removeAction(RoutingControlAction.class);
959                int newPath = mService.portIdToPath(getActivePortId());
960                setActivePath(newPath);
961                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
962                        getActivePath(), newPath));
963                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
964                        routingForBootup, null));
965            }
966        } else {
967            int activePath = mService.getPhysicalAddress();
968            setActivePath(activePath);
969            if (!routingForBootup) {
970                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
971                        activePath));
972            }
973        }
974    }
975
976    /**
977     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
978     * the given routing path. CEC devices use routing path for its physical address to
979     * describe the hierarchy of the devices in the network.
980     *
981     * @param path routing path or physical address
982     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
983     */
984    @ServiceThreadOnly
985    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
986        assertRunOnServiceThread();
987        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
988            if (info.getPhysicalAddress() == path) {
989                return info;
990            }
991        }
992        return null;
993    }
994
995    /**
996     * Whether a device of the specified physical address and logical address exists
997     * in a device info list. However, both are minimal condition and it could
998     * be different device from the original one.
999     *
1000     * @param logicalAddress logical address of a device to be searched
1001     * @param physicalAddress physical address of a device to be searched
1002     * @return true if exist; otherwise false
1003     */
1004    @ServiceThreadOnly
1005    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1006        assertRunOnServiceThread();
1007        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
1008        if (device == null) {
1009            return false;
1010        }
1011        return device.getPhysicalAddress() == physicalAddress;
1012    }
1013
1014    @Override
1015    @ServiceThreadOnly
1016    void onHotplug(int portId, boolean connected) {
1017        assertRunOnServiceThread();
1018
1019        // Tv device will have permanent HotplugDetectionAction.
1020        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1021        if (!hotplugActions.isEmpty()) {
1022            // Note that hotplug action is single action running on a machine.
1023            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1024            // It covers seq #40, #43.
1025            hotplugActions.get(0).pollAllDevicesNow();
1026        }
1027    }
1028
1029    @ServiceThreadOnly
1030    void setAutoDeviceOff(boolean enabled) {
1031        assertRunOnServiceThread();
1032        mAutoDeviceOff = enabled;
1033    }
1034
1035    @Override
1036    @ServiceThreadOnly
1037    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1038        super.disableDevice(initiatedByCec, callback);
1039        assertRunOnServiceThread();
1040        // Remove any repeated working actions.
1041        // HotplugDetectionAction will be reinstated during the wake up process.
1042        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1043        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1044        removeAction(DeviceDiscoveryAction.class);
1045        removeAction(HotplugDetectionAction.class);
1046
1047        disableSystemAudioIfExist();
1048        disableArcIfExist();
1049        checkIfPendingActionsCleared();
1050    }
1051
1052    @ServiceThreadOnly
1053    private void disableSystemAudioIfExist() {
1054        assertRunOnServiceThread();
1055        if (getAvrDeviceInfo() == null) {
1056            return;
1057        }
1058
1059        // Seq #31.
1060        removeAction(SystemAudioActionFromAvr.class);
1061        removeAction(SystemAudioActionFromTv.class);
1062        removeAction(SystemAudioAutoInitiationAction.class);
1063        removeAction(SystemAudioStatusAction.class);
1064        removeAction(VolumeControlAction.class);
1065
1066        // Once adding additional param which describes whether to record it to NVM or not to this
1067        // method, put "false" for it.
1068        setSystemAudioMode(false);
1069    }
1070
1071    @ServiceThreadOnly
1072    private void disableArcIfExist() {
1073        assertRunOnServiceThread();
1074        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
1075        if (avr == null) {
1076            return;
1077        }
1078
1079        // Seq #44.
1080        removeAction(RequestArcInitiationAction.class);
1081        if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1082            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1083        }
1084    }
1085
1086    @Override
1087    @ServiceThreadOnly
1088    protected void onStandby(boolean initiatedByCec) {
1089        assertRunOnServiceThread();
1090        // Seq #11
1091        if (!mService.isControlEnabled()) {
1092            return;
1093        }
1094        if (!initiatedByCec) {
1095            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1096                    mAddress, Constants.ADDR_BROADCAST));
1097        }
1098    }
1099
1100    @Override
1101    @ServiceThreadOnly
1102    protected boolean handleStandby(HdmiCecMessage message) {
1103        assertRunOnServiceThread();
1104        // Seq #12
1105        // Tv accepts directly addressed <Standby> only.
1106        if (message.getDestination() == mAddress) {
1107            super.handleStandby(message);
1108        }
1109        return false;
1110    }
1111
1112    boolean isProhibitMode() {
1113        return mService.isProhibitMode();
1114    }
1115
1116    boolean isPowerStandbyOrTransient() {
1117        return mService.isPowerStandbyOrTransient();
1118    }
1119
1120    void displayOsd(int messageId) {
1121        Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE);
1122        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId);
1123        mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
1124                HdmiControlService.PERMISSION);
1125    }
1126}
1127