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