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