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