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