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