HdmiCecLocalDeviceTv.java revision 5ad57168da6456e8e4935aaa8512a7f77b74b0a1
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        mDeviceInfos.clear();
560        updateSafeDeviceInfoList();
561    }
562
563    @ServiceThreadOnly
564    // Seq #32
565    void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
566        assertRunOnServiceThread();
567        if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) {
568            setSystemAudioMode(false, true);
569            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
570            return;
571        }
572        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
573        if (avr == null) {
574            setSystemAudioMode(false, true);
575            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
576            return;
577        }
578
579        addAndStartAction(
580                new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback));
581    }
582
583    // # Seq 25
584    void setSystemAudioMode(boolean on, boolean updateSetting) {
585        if (updateSetting) {
586            mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on);
587        }
588        updateAudioManagerForSystemAudio(on);
589        synchronized (mLock) {
590            if (mSystemAudioActivated != on) {
591                mSystemAudioActivated = on;
592                mService.announceSystemAudioModeChange(on);
593            }
594        }
595    }
596
597    private void updateAudioManagerForSystemAudio(boolean on) {
598        // TODO: remove output device, once update AudioManager api.
599        mService.getAudioManager().setHdmiSystemAudioSupported(on);
600    }
601
602    boolean isSystemAudioActivated() {
603        if (getAvrDeviceInfo() == null) {
604            return false;
605        }
606        synchronized (mLock) {
607            return mSystemAudioActivated;
608        }
609    }
610
611    boolean getSystemAudioModeSetting() {
612        return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false);
613    }
614
615    /**
616     * Change ARC status into the given {@code enabled} status.
617     *
618     * @return {@code true} if ARC was in "Enabled" status
619     */
620    @ServiceThreadOnly
621    boolean setArcStatus(boolean enabled) {
622        assertRunOnServiceThread();
623        boolean oldStatus = mArcEstablished;
624        // 1. Enable/disable ARC circuit.
625        mService.setAudioReturnChannel(enabled);
626        // 2. Notify arc status to audio service.
627        notifyArcStatusToAudioService(enabled);
628        // 3. Update arc status;
629        mArcEstablished = enabled;
630        return oldStatus;
631    }
632
633    private void notifyArcStatusToAudioService(boolean enabled) {
634        // Note that we don't set any name to ARC.
635        mService.getAudioManager().setWiredDeviceConnectionState(
636                AudioSystem.DEVICE_OUT_HDMI_ARC,
637                enabled ? 1 : 0, "");
638    }
639
640    /**
641     * Returns whether ARC is enabled or not.
642     */
643    @ServiceThreadOnly
644    boolean isArcEstabilished() {
645        assertRunOnServiceThread();
646        return mArcFeatureEnabled && mArcEstablished;
647    }
648
649    @ServiceThreadOnly
650    void changeArcFeatureEnabled(boolean enabled) {
651        assertRunOnServiceThread();
652
653        if (mArcFeatureEnabled != enabled) {
654            if (enabled) {
655                if (!mArcEstablished) {
656                    startArcAction(true);
657                }
658            } else {
659                if (mArcEstablished) {
660                    startArcAction(false);
661                }
662            }
663            mArcFeatureEnabled = enabled;
664        }
665    }
666
667    @ServiceThreadOnly
668    boolean isArcFeatureEnabled() {
669        assertRunOnServiceThread();
670        return mArcFeatureEnabled;
671    }
672
673    @ServiceThreadOnly
674    private void startArcAction(boolean enabled) {
675        assertRunOnServiceThread();
676        HdmiCecDeviceInfo info = getAvrDeviceInfo();
677        if (info == null) {
678            return;
679        }
680        if (!isConnectedToArcPort(info.getPhysicalAddress())) {
681            return;
682        }
683
684        // Terminate opposite action and start action if not exist.
685        if (enabled) {
686            removeAction(RequestArcTerminationAction.class);
687            if (!hasAction(RequestArcInitiationAction.class)) {
688                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
689            }
690        } else {
691            removeAction(RequestArcInitiationAction.class);
692            if (!hasAction(RequestArcTerminationAction.class)) {
693                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
694            }
695        }
696    }
697
698    void setAudioStatus(boolean mute, int volume) {
699        synchronized (mLock) {
700            mSystemAudioMute = mute;
701            mSystemAudioVolume = volume;
702            int maxVolume = mService.getAudioManager().getStreamMaxVolume(
703                    AudioManager.STREAM_MUSIC);
704            mService.setAudioStatus(mute,
705                    VolumeControlAction.scaleToCustomVolume(volume, maxVolume));
706        }
707    }
708
709    @ServiceThreadOnly
710    void changeVolume(int curVolume, int delta, int maxVolume) {
711        assertRunOnServiceThread();
712        if (delta == 0 || !isSystemAudioActivated()) {
713            return;
714        }
715
716        int targetVolume = curVolume + delta;
717        int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume);
718        synchronized (mLock) {
719            // If new volume is the same as current system audio volume, just ignore it.
720            // Note that UNKNOWN_VOLUME is not in range of cec volume scale.
721            if (cecVolume == mSystemAudioVolume) {
722                // Update tv volume with system volume value.
723                mService.setAudioStatus(false,
724                        VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume));
725                return;
726            }
727        }
728
729        // Remove existing volume action.
730        removeAction(VolumeControlAction.class);
731
732        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
733        addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(),
734                cecVolume, delta > 0));
735    }
736
737    @ServiceThreadOnly
738    void changeMute(boolean mute) {
739        assertRunOnServiceThread();
740        if (!isSystemAudioActivated()) {
741            return;
742        }
743
744        // Remove existing volume action.
745        removeAction(VolumeControlAction.class);
746        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
747        addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute));
748    }
749
750    private boolean isSystemAudioOn() {
751
752
753        synchronized (mLock) {
754            return mSystemAudioActivated;
755        }
756    }
757
758    @Override
759    @ServiceThreadOnly
760    protected boolean handleInitiateArc(HdmiCecMessage message) {
761        assertRunOnServiceThread();
762        // In case where <Initiate Arc> is started by <Request ARC Initiation>
763        // need to clean up RequestArcInitiationAction.
764        removeAction(RequestArcInitiationAction.class);
765        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
766                message.getSource(), true);
767        addAndStartAction(action);
768        return true;
769    }
770
771    @Override
772    @ServiceThreadOnly
773    protected boolean handleTerminateArc(HdmiCecMessage message) {
774        assertRunOnServiceThread();
775        // In case where <Terminate Arc> is started by <Request ARC Termination>
776        // need to clean up RequestArcInitiationAction.
777        removeAction(RequestArcTerminationAction.class);
778        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
779                message.getSource(), false);
780        addAndStartAction(action);
781        return true;
782    }
783
784    @Override
785    @ServiceThreadOnly
786    protected boolean handleSetSystemAudioMode(HdmiCecMessage message) {
787        assertRunOnServiceThread();
788        if (!isMessageForSystemAudio(message)) {
789            return false;
790        }
791        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
792                message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null);
793        addAndStartAction(action);
794        return true;
795    }
796
797    @Override
798    @ServiceThreadOnly
799    protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) {
800        assertRunOnServiceThread();
801        if (!isMessageForSystemAudio(message)) {
802            return false;
803        }
804        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true);
805        return true;
806    }
807
808    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
809        if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM
810                || message.getDestination() != Constants.ADDR_TV
811                || getAvrDeviceInfo() == null) {
812            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
813            return false;
814        }
815        return true;
816    }
817
818    /**
819     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
820     * logical address as new device info's.
821     *
822     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
823     *
824     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
825     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
826     *         that has the same logical address as new one has.
827     */
828    @ServiceThreadOnly
829    private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
830        assertRunOnServiceThread();
831        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
832        if (oldDeviceInfo != null) {
833            removeDeviceInfo(deviceInfo.getLogicalAddress());
834        }
835        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
836        updateSafeDeviceInfoList();
837        return oldDeviceInfo;
838    }
839
840    /**
841     * Remove a device info corresponding to the given {@code logicalAddress}.
842     * It returns removed {@link HdmiCecDeviceInfo} if exists.
843     *
844     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
845     *
846     * @param logicalAddress logical address of device to be removed
847     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
848     */
849    @ServiceThreadOnly
850    private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
851        assertRunOnServiceThread();
852        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
853        if (deviceInfo != null) {
854            mDeviceInfos.remove(logicalAddress);
855        }
856        updateSafeDeviceInfoList();
857        return deviceInfo;
858    }
859
860    /**
861     * Return a list of all {@link HdmiCecDeviceInfo}.
862     *
863     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
864     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}.
865     */
866    @ServiceThreadOnly
867    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) {
868        assertRunOnServiceThread();
869        if (includelLocalDevice) {
870            return HdmiUtils.sparseArrayToList(mDeviceInfos);
871        } else {
872            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
873            for (int i = 0; i < mDeviceInfos.size(); ++i) {
874                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
875                if (!isLocalDeviceAddress(info.getLogicalAddress())) {
876                    infoList.add(info);
877                }
878            }
879            return infoList;
880        }
881    }
882
883    /**
884     * Return external input devices.
885     */
886    List<HdmiCecDeviceInfo> getSafeExternalInputs() {
887        synchronized (mLock) {
888            return mSafeExternalInputs;
889        }
890    }
891
892    @ServiceThreadOnly
893    private void updateSafeDeviceInfoList() {
894        assertRunOnServiceThread();
895        List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos);
896        List<HdmiCecDeviceInfo> externalInputs = getInputDevices();
897        synchronized (mLock) {
898            mSafeAllDeviceInfos = copiedDevices;
899            mSafeExternalInputs = externalInputs;
900        }
901    }
902
903    /**
904     * Return a list of external cec input (source) devices.
905     *
906     * <p>Note that this effectively excludes non-source devices like system audio,
907     * secondary TV.
908     */
909    private List<HdmiCecDeviceInfo> getInputDevices() {
910        ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
911        for (int i = 0; i < mDeviceInfos.size(); ++i) {
912            HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
913            if (isLocalDeviceAddress(i)) {
914                continue;
915            }
916            if (info.isSourceType()) {
917                infoList.add(info);
918            }
919        }
920        return infoList;
921    }
922
923    @ServiceThreadOnly
924    private boolean isLocalDeviceAddress(int address) {
925        assertRunOnServiceThread();
926        for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) {
927            if (device.isAddressOf(address)) {
928                return true;
929            }
930        }
931        return false;
932    }
933
934    @ServiceThreadOnly
935    HdmiCecDeviceInfo getAvrDeviceInfo() {
936        assertRunOnServiceThread();
937        return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
938    }
939
940    /**
941     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
942     *
943     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
944     * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}.
945     *
946     * @param logicalAddress logical address to be retrieved
947     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
948     *         Returns null if no logical address matched
949     */
950    @ServiceThreadOnly
951    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
952        assertRunOnServiceThread();
953        return mDeviceInfos.get(logicalAddress);
954    }
955
956    boolean hasSystemAudioDevice() {
957        return getSafeAvrDeviceInfo() != null;
958    }
959
960    HdmiCecDeviceInfo getSafeAvrDeviceInfo() {
961        return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
962    }
963
964    /**
965     * Thread safe version of {@link #getDeviceInfo(int)}.
966     *
967     * @param logicalAddress logical address to be retrieved
968     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
969     *         Returns null if no logical address matched
970     */
971    HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) {
972        synchronized (mLock) {
973            return mSafeAllDeviceInfos.get(logicalAddress);
974        }
975    }
976
977    /**
978     * Called when a device is newly added or a new device is detected or
979     * existing device is updated.
980     *
981     * @param info device info of a new device.
982     */
983    @ServiceThreadOnly
984    final void addCecDevice(HdmiCecDeviceInfo info) {
985        assertRunOnServiceThread();
986        addDeviceInfo(info);
987        if (info.getLogicalAddress() == mAddress) {
988            // The addition of TV device itself should not be notified.
989            return;
990        }
991        mService.invokeDeviceEventListeners(info, true);
992    }
993
994    /**
995     * Called when a device is removed or removal of device is detected.
996     *
997     * @param address a logical address of a device to be removed
998     */
999    @ServiceThreadOnly
1000    final void removeCecDevice(int address) {
1001        assertRunOnServiceThread();
1002        HdmiCecDeviceInfo info = removeDeviceInfo(address);
1003
1004        mCecMessageCache.flushMessagesFrom(address);
1005        mService.invokeDeviceEventListeners(info, false);
1006    }
1007
1008    @ServiceThreadOnly
1009    void handleRemoveActiveRoutingPath(int path) {
1010        assertRunOnServiceThread();
1011        // Seq #23
1012        if (isTailOfActivePath(path, getActivePath())) {
1013            removeAction(RoutingControlAction.class);
1014            int newPath = mService.portIdToPath(getActivePortId());
1015            mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(
1016                    mAddress, getActivePath(), newPath));
1017            addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null));
1018        }
1019    }
1020
1021    /**
1022     * Launch routing control process.
1023     *
1024     * @param routingForBootup true if routing control is initiated due to One Touch Play
1025     *        or TV power on
1026     */
1027    @ServiceThreadOnly
1028    void launchRoutingControl(boolean routingForBootup) {
1029        assertRunOnServiceThread();
1030        // Seq #24
1031        if (getActivePortId() != Constants.INVALID_PORT_ID) {
1032            if (!routingForBootup && !isProhibitMode()) {
1033                removeAction(RoutingControlAction.class);
1034                int newPath = mService.portIdToPath(getActivePortId());
1035                setActivePath(newPath);
1036                mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress,
1037                        getActivePath(), newPath));
1038                addAndStartAction(new RoutingControlAction(this, getActivePortId(),
1039                        routingForBootup, null));
1040            }
1041        } else {
1042            int activePath = mService.getPhysicalAddress();
1043            setActivePath(activePath);
1044            if (!routingForBootup) {
1045                mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress,
1046                        activePath));
1047            }
1048        }
1049    }
1050
1051    /**
1052     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
1053     * the given routing path. CEC devices use routing path for its physical address to
1054     * describe the hierarchy of the devices in the network.
1055     *
1056     * @param path routing path or physical address
1057     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
1058     */
1059    @ServiceThreadOnly
1060    final HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
1061        assertRunOnServiceThread();
1062        for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) {
1063            if (info.getPhysicalAddress() == path) {
1064                return info;
1065            }
1066        }
1067        return null;
1068    }
1069
1070    /**
1071     * Whether a device of the specified physical address and logical address exists
1072     * in a device info list. However, both are minimal condition and it could
1073     * be different device from the original one.
1074     *
1075     * @param logicalAddress logical address of a device to be searched
1076     * @param physicalAddress physical address of a device to be searched
1077     * @return true if exist; otherwise false
1078     */
1079    @ServiceThreadOnly
1080    boolean isInDeviceList(int logicalAddress, int physicalAddress) {
1081        assertRunOnServiceThread();
1082        HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress);
1083        if (device == null) {
1084            return false;
1085        }
1086        return device.getPhysicalAddress() == physicalAddress;
1087    }
1088
1089    @Override
1090    @ServiceThreadOnly
1091    void onHotplug(int portId, boolean connected) {
1092        assertRunOnServiceThread();
1093
1094        // Tv device will have permanent HotplugDetectionAction.
1095        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
1096        if (!hotplugActions.isEmpty()) {
1097            // Note that hotplug action is single action running on a machine.
1098            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
1099            // It covers seq #40, #43.
1100            hotplugActions.get(0).pollAllDevicesNow();
1101        }
1102    }
1103
1104    @ServiceThreadOnly
1105    void setAutoDeviceOff(boolean enabled) {
1106        assertRunOnServiceThread();
1107        mAutoDeviceOff = enabled;
1108        mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, enabled);
1109    }
1110
1111    @ServiceThreadOnly
1112    void setAutoWakeup(boolean enabled) {
1113        assertRunOnServiceThread();
1114        mAutoWakeup = enabled;
1115        mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, enabled);
1116    }
1117
1118    @Override
1119    @ServiceThreadOnly
1120    protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
1121        super.disableDevice(initiatedByCec, callback);
1122        assertRunOnServiceThread();
1123        // Remove any repeated working actions.
1124        // HotplugDetectionAction will be reinstated during the wake up process.
1125        // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
1126        //     LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery().
1127        removeAction(DeviceDiscoveryAction.class);
1128        removeAction(HotplugDetectionAction.class);
1129
1130        disableSystemAudioIfExist();
1131        disableArcIfExist();
1132        checkIfPendingActionsCleared();
1133    }
1134
1135    @ServiceThreadOnly
1136    private void disableSystemAudioIfExist() {
1137        assertRunOnServiceThread();
1138        if (getAvrDeviceInfo() == null) {
1139            return;
1140        }
1141
1142        // Seq #31.
1143        removeAction(SystemAudioActionFromAvr.class);
1144        removeAction(SystemAudioActionFromTv.class);
1145        removeAction(SystemAudioAutoInitiationAction.class);
1146        removeAction(SystemAudioStatusAction.class);
1147        removeAction(VolumeControlAction.class);
1148
1149        // Turn off the mode but do not write it the settings, so that the next time TV powers on
1150        // the system audio mode setting can be restored automatically.
1151        setSystemAudioMode(false, false);
1152    }
1153
1154    @ServiceThreadOnly
1155    private void disableArcIfExist() {
1156        assertRunOnServiceThread();
1157        HdmiCecDeviceInfo avr = getAvrDeviceInfo();
1158        if (avr == null) {
1159            return;
1160        }
1161
1162        // Seq #44.
1163        removeAction(RequestArcInitiationAction.class);
1164        if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) {
1165            addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress()));
1166        }
1167    }
1168
1169    @Override
1170    @ServiceThreadOnly
1171    protected void onStandby(boolean initiatedByCec) {
1172        assertRunOnServiceThread();
1173        // Seq #11
1174        if (!mService.isControlEnabled()) {
1175            return;
1176        }
1177        if (!initiatedByCec && mAutoDeviceOff) {
1178            mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby(
1179                    mAddress, Constants.ADDR_BROADCAST));
1180        }
1181    }
1182
1183    @Override
1184    @ServiceThreadOnly
1185    protected boolean handleStandby(HdmiCecMessage message) {
1186        assertRunOnServiceThread();
1187        // Seq #12
1188        // Tv accepts directly addressed <Standby> only.
1189        if (message.getDestination() == mAddress) {
1190            super.handleStandby(message);
1191        }
1192        return false;
1193    }
1194
1195    boolean isProhibitMode() {
1196        return mService.isProhibitMode();
1197    }
1198
1199    boolean isPowerStandbyOrTransient() {
1200        return mService.isPowerStandbyOrTransient();
1201    }
1202
1203    void displayOsd(int messageId) {
1204        Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE);
1205        intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId);
1206        mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
1207                HdmiControlService.PERMISSION);
1208    }
1209}
1210