HdmiControlService.java revision 2b152015ff94f20b9ec3ef284fb83105f8b3c831
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.annotation.Nullable;
20import android.content.BroadcastReceiver;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.hardware.hdmi.HdmiCecDeviceInfo;
26import android.hardware.hdmi.HdmiControlManager;
27import android.hardware.hdmi.HdmiHotplugEvent;
28import android.hardware.hdmi.HdmiPortInfo;
29import android.hardware.hdmi.HdmiTvClient;
30import android.hardware.hdmi.IHdmiControlCallback;
31import android.hardware.hdmi.IHdmiControlService;
32import android.hardware.hdmi.IHdmiDeviceEventListener;
33import android.hardware.hdmi.IHdmiHotplugEventListener;
34import android.hardware.hdmi.IHdmiInputChangeListener;
35import android.hardware.hdmi.IHdmiRecordRequestListener;
36import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
37import android.hardware.hdmi.IHdmiVendorCommandListener;
38import android.media.AudioManager;
39import android.os.Build;
40import android.os.Handler;
41import android.os.HandlerThread;
42import android.os.IBinder;
43import android.os.Looper;
44import android.os.PowerManager;
45import android.os.RemoteException;
46import android.os.SystemClock;
47import android.provider.Settings.Global;
48import android.util.ArraySet;
49import android.util.Slog;
50import android.util.SparseArray;
51import android.util.SparseIntArray;
52
53import com.android.internal.annotations.GuardedBy;
54import com.android.server.SystemService;
55import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
56import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback;
57import com.android.server.hdmi.HdmiCecLocalDevice.PendingActionClearedCallback;
58
59import libcore.util.EmptyArray;
60
61import java.util.ArrayList;
62import java.util.Collections;
63import java.util.List;
64
65/**
66 * Provides a service for sending and processing HDMI control messages,
67 * HDMI-CEC and MHL control command, and providing the information on both standard.
68 */
69public final class HdmiControlService extends SystemService {
70    private static final String TAG = "HdmiControlService";
71
72    static final String PERMISSION = "android.permission.HDMI_CEC";
73
74    /**
75     * Interface to report send result.
76     */
77    interface SendMessageCallback {
78        /**
79         * Called when {@link HdmiControlService#sendCecCommand} is completed.
80         *
81         * @param error result of send request.
82         * <ul>
83         * <li>{@link Constants#SEND_RESULT_SUCCESS}
84         * <li>{@link Constants#SEND_RESULT_NAK}
85         * <li>{@link Constants#SEND_RESULT_FAILURE}
86         * </ul>
87         */
88        void onSendCompleted(int error);
89    }
90
91    /**
92     * Interface to get a list of available logical devices.
93     */
94    interface DevicePollingCallback {
95        /**
96         * Called when device polling is finished.
97         *
98         * @param ackedAddress a list of logical addresses of available devices
99         */
100        void onPollingFinished(List<Integer> ackedAddress);
101    }
102
103    private class PowerStateReceiver extends BroadcastReceiver {
104        @Override
105        public void onReceive(Context context, Intent intent) {
106            switch (intent.getAction()) {
107                case Intent.ACTION_SCREEN_OFF:
108                    if (isPowerOnOrTransient()) {
109                        onStandby();
110                    }
111                    break;
112                case Intent.ACTION_SCREEN_ON:
113                    if (isPowerStandbyOrTransient()) {
114                        onWakeUp();
115                    }
116                    break;
117            }
118        }
119    }
120
121    // A thread to handle synchronous IO of CEC and MHL control service.
122    // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms)
123    // and sparse call it shares a thread to handle IO operations.
124    private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread");
125
126    // Used to synchronize the access to the service.
127    private final Object mLock = new Object();
128
129    // Type of logical devices hosted in the system. Stored in the unmodifiable list.
130    private final List<Integer> mLocalDevices;
131
132    // List of listeners registered by callers that want to get notified of
133    // hotplug events.
134    @GuardedBy("mLock")
135    private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>();
136
137    // List of records for hotplug event listener to handle the the caller killed in action.
138    @GuardedBy("mLock")
139    private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
140            new ArrayList<>();
141
142    // List of listeners registered by callers that want to get notified of
143    // device status events.
144    @GuardedBy("mLock")
145    private final ArrayList<IHdmiDeviceEventListener> mDeviceEventListeners = new ArrayList<>();
146
147    // List of records for device event listener to handle the the caller killed in action.
148    @GuardedBy("mLock")
149    private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords =
150            new ArrayList<>();
151
152    // List of records for vendor command listener to handle the the caller killed in action.
153    @GuardedBy("mLock")
154    private final ArrayList<VendorCommandListenerRecord> mVendorCommandListenerRecords =
155            new ArrayList<>();
156
157    @GuardedBy("mLock")
158    private IHdmiInputChangeListener mInputChangeListener;
159
160    @GuardedBy("mLock")
161    private InputChangeListenerRecord mInputChangeListenerRecord;
162
163    @GuardedBy("mLock")
164    private IHdmiRecordRequestListener mRecordRequestListener;
165
166    @GuardedBy("mLock")
167    private HdmiRecordRequestListenerRecord mRecordRequestListenerRecord;
168
169    // Set to true while HDMI control is enabled. If set to false, HDMI-CEC/MHL protocol
170    // handling will be disabled and no request will be handled.
171    @GuardedBy("mLock")
172    private boolean mHdmiControlEnabled;
173
174    // Set to true while the service is in normal mode. While set to false, no input change is
175    // allowed. Used for situations where input change can confuse users such as channel auto-scan,
176    // system upgrade, etc., a.k.a. "prohibit mode".
177    @GuardedBy("mLock")
178    private boolean mProhibitMode;
179
180    // List of listeners registered by callers that want to get notified of
181    // system audio mode changes.
182    private final ArrayList<IHdmiSystemAudioModeChangeListener>
183            mSystemAudioModeChangeListeners = new ArrayList<>();
184    // List of records for system audio mode change to handle the the caller killed in action.
185    private final ArrayList<SystemAudioModeChangeListenerRecord>
186            mSystemAudioModeChangeListenerRecords = new ArrayList<>();
187
188    // Handler used to run a task in service thread.
189    private final Handler mHandler = new Handler();
190
191    @Nullable
192    private HdmiCecController mCecController;
193
194    @Nullable
195    private HdmiMhlController mMhlController;
196
197    // HDMI port information. Stored in the unmodifiable list to keep the static information
198    // from being modified.
199    private List<HdmiPortInfo> mPortInfo;
200
201    // Map from path(physical address) to port ID.
202    private SparseIntArray mPortIdMap = new SparseIntArray();
203
204    // Map from port ID to HdmiPortInfo.
205    private SparseArray<HdmiPortInfo> mPortInfoMap = new SparseArray<>();
206
207    private HdmiCecMessageValidator mMessageValidator;
208
209    private final PowerStateReceiver mPowerStateReceiver = new PowerStateReceiver();
210
211    @ServiceThreadOnly
212    private int mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY;
213
214    @ServiceThreadOnly
215    private boolean mStandbyMessageReceived = false;
216
217    public HdmiControlService(Context context) {
218        super(context);
219        mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray(
220                com.android.internal.R.array.config_hdmiCecLogicalDeviceType));
221    }
222
223    @Override
224    public void onStart() {
225        mIoThread.start();
226        mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
227        mProhibitMode = false;
228        mHdmiControlEnabled = readBooleanSetting(Global.HDMI_CONTROL_ENABLED, true);
229
230        mCecController = HdmiCecController.create(this);
231        if (mCecController != null) {
232            // TODO: Remove this as soon as OEM's HAL implementation is corrected.
233            mCecController.setOption(HdmiTvClient.OPTION_CEC_ENABLE,
234                    HdmiTvClient.ENABLED);
235
236            // TODO: load value for mHdmiControlEnabled from preference.
237            if (mHdmiControlEnabled) {
238                initializeCec(true);
239            }
240        } else {
241            Slog.i(TAG, "Device does not support HDMI-CEC.");
242        }
243
244        mMhlController = HdmiMhlController.create(this);
245        if (mMhlController == null) {
246            Slog.i(TAG, "Device does not support MHL-control.");
247        }
248        initPortInfo();
249        mMessageValidator = new HdmiCecMessageValidator(this);
250        publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService());
251
252        // Register broadcast receiver for power state change.
253        if (mCecController != null || mMhlController != null) {
254            IntentFilter filter = new IntentFilter();
255            filter.addAction(Intent.ACTION_SCREEN_OFF);
256            filter.addAction(Intent.ACTION_SCREEN_ON);
257            getContext().registerReceiver(mPowerStateReceiver, filter);
258        }
259    }
260
261    boolean readBooleanSetting(String key, boolean defVal) {
262        ContentResolver cr = getContext().getContentResolver();
263        return Global.getInt(cr, key, defVal ? Constants.TRUE : Constants.FALSE) == Constants.TRUE;
264    }
265
266    void writeBooleanSetting(String key, boolean value) {
267        ContentResolver cr = getContext().getContentResolver();
268        Global.putInt(cr, key, value ? Constants.TRUE : Constants.FALSE);
269    }
270
271    private void initializeCec(boolean fromBootup) {
272        mCecController.setOption(HdmiTvClient.OPTION_CEC_SERVICE_CONTROL,
273                HdmiTvClient.ENABLED);
274        initializeLocalDevices(mLocalDevices, fromBootup);
275    }
276
277    @ServiceThreadOnly
278    private void initializeLocalDevices(final List<Integer> deviceTypes, final boolean fromBootup) {
279        assertRunOnServiceThread();
280        // A container for [Logical Address, Local device info].
281        final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>();
282        final SparseIntArray finished = new SparseIntArray();
283        mCecController.clearLogicalAddress();
284        for (int type : deviceTypes) {
285            final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type);
286            localDevice.init();
287            mCecController.allocateLogicalAddress(type,
288                    localDevice.getPreferredAddress(), new AllocateAddressCallback() {
289                @Override
290                public void onAllocated(int deviceType, int logicalAddress) {
291                    if (logicalAddress == Constants.ADDR_UNREGISTERED) {
292                        Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]");
293                    } else {
294                        HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType);
295                        localDevice.setDeviceInfo(deviceInfo);
296                        mCecController.addLocalDevice(deviceType, localDevice);
297                        mCecController.addLogicalAddress(logicalAddress);
298                        devices.append(logicalAddress, localDevice);
299                    }
300                    finished.append(deviceType, logicalAddress);
301
302                    // Address allocation completed for all devices. Notify each device.
303                    if (deviceTypes.size() == finished.size()) {
304                        if (mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON) {
305                            mPowerStatus = HdmiControlManager.POWER_STATUS_ON;
306                        }
307                        notifyAddressAllocated(devices, fromBootup);
308                    }
309                }
310            });
311        }
312    }
313
314    @ServiceThreadOnly
315    private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices,
316            boolean fromBootup) {
317        assertRunOnServiceThread();
318        for (int i = 0; i < devices.size(); ++i) {
319            int address = devices.keyAt(i);
320            HdmiCecLocalDevice device = devices.valueAt(i);
321            device.handleAddressAllocated(address, fromBootup);
322        }
323    }
324
325    // Initialize HDMI port information. Combine the information from CEC and MHL HAL and
326    // keep them in one place.
327    @ServiceThreadOnly
328    private void initPortInfo() {
329        assertRunOnServiceThread();
330        HdmiPortInfo[] cecPortInfo = null;
331
332        // CEC HAL provides majority of the info while MHL does only MHL support flag for
333        // each port. Return empty array if CEC HAL didn't provide the info.
334        if (mCecController != null) {
335            cecPortInfo = mCecController.getPortInfos();
336        }
337        if (cecPortInfo == null) {
338            return;
339        }
340
341        for (HdmiPortInfo info : cecPortInfo) {
342            mPortIdMap.put(info.getAddress(), info.getId());
343            mPortInfoMap.put(info.getId(), info);
344        }
345
346        HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0];
347        if (mMhlController != null) {
348            // TODO: Implement plumbing logic to get MHL port information.
349            // mhlPortInfo = mMhlController.getPortInfos();
350        }
351
352        ArraySet<Integer> mhlSupportedPorts = new ArraySet<Integer>(mhlPortInfo.length);
353        for (HdmiPortInfo info : mhlPortInfo) {
354            if (info.isMhlSupported()) {
355                mhlSupportedPorts.add(info.getId());
356            }
357        }
358
359        // Build HDMI port info list with CEC port info plus MHL supported flag.
360        ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length);
361        for (HdmiPortInfo info : cecPortInfo) {
362            if (mhlSupportedPorts.contains(info.getId())) {
363                result.add(new HdmiPortInfo(info.getId(), info.getType(), info.getAddress(),
364                        info.isCecSupported(), true, info.isArcSupported()));
365            } else {
366                result.add(info);
367            }
368        }
369        mPortInfo = Collections.unmodifiableList(result);
370    }
371
372    /**
373     * Returns HDMI port information for the given port id.
374     *
375     * @param portId HDMI port id
376     * @return {@link HdmiPortInfo} for the given port
377     */
378    @ServiceThreadOnly
379    HdmiPortInfo getPortInfo(int portId) {
380        assertRunOnServiceThread();
381        return mPortInfoMap.get(portId, null);
382    }
383
384    /**
385     * Returns the routing path (physical address) of the HDMI port for the given
386     * port id.
387     */
388    @ServiceThreadOnly
389    int portIdToPath(int portId) {
390        assertRunOnServiceThread();
391        HdmiPortInfo portInfo = getPortInfo(portId);
392        if (portInfo == null) {
393            Slog.e(TAG, "Cannot find the port info: " + portId);
394            return Constants.INVALID_PHYSICAL_ADDRESS;
395        }
396        return portInfo.getAddress();
397    }
398
399    /**
400     * Returns the id of HDMI port located at the top of the hierarchy of
401     * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance,
402     * the port id to be returned is the ID associated with the port address
403     * 0x1000 (1.0.0.0) which is the topmost path of the given routing path.
404     */
405    @ServiceThreadOnly
406    int pathToPortId(int path) {
407        assertRunOnServiceThread();
408        int portAddress = path & Constants.ROUTING_PATH_TOP_MASK;
409        return mPortIdMap.get(portAddress, Constants.INVALID_PORT_ID);
410    }
411
412    @ServiceThreadOnly
413    boolean isValidPortId(int portId) {
414        assertRunOnServiceThread();
415        return getPortInfo(portId) != null;
416    }
417
418    /**
419     * Returns {@link Looper} for IO operation.
420     *
421     * <p>Declared as package-private.
422     */
423    Looper getIoLooper() {
424        return mIoThread.getLooper();
425    }
426
427    /**
428     * Returns {@link Looper} of main thread. Use this {@link Looper} instance
429     * for tasks that are running on main service thread.
430     *
431     * <p>Declared as package-private.
432     */
433    Looper getServiceLooper() {
434        return mHandler.getLooper();
435    }
436
437    /**
438     * Returns physical address of the device.
439     */
440    int getPhysicalAddress() {
441        return mCecController.getPhysicalAddress();
442    }
443
444    /**
445     * Returns vendor id of CEC service.
446     */
447    int getVendorId() {
448        return mCecController.getVendorId();
449    }
450
451    @ServiceThreadOnly
452    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
453        assertRunOnServiceThread();
454        HdmiCecLocalDeviceTv tv = tv();
455        if (tv == null) {
456            return null;
457        }
458        return tv.getDeviceInfo(logicalAddress);
459    }
460
461    /**
462     * Returns version of CEC.
463     */
464    int getCecVersion() {
465        return mCecController.getVersion();
466    }
467
468    /**
469     * Whether a device of the specified physical address is connected to ARC enabled port.
470     */
471    @ServiceThreadOnly
472    boolean isConnectedToArcPort(int physicalAddress) {
473        assertRunOnServiceThread();
474        int portId = mPortIdMap.get(physicalAddress);
475        if (portId != Constants.INVALID_PORT_ID) {
476            return mPortInfoMap.get(portId).isArcSupported();
477        }
478        return false;
479    }
480
481    void runOnServiceThread(Runnable runnable) {
482        mHandler.post(runnable);
483    }
484
485    void runOnServiceThreadAtFrontOfQueue(Runnable runnable) {
486        mHandler.postAtFrontOfQueue(runnable);
487    }
488
489    private void assertRunOnServiceThread() {
490        if (Looper.myLooper() != mHandler.getLooper()) {
491            throw new IllegalStateException("Should run on service thread.");
492        }
493    }
494
495    /**
496     * Transmit a CEC command to CEC bus.
497     *
498     * @param command CEC command to send out
499     * @param callback interface used to the result of send command
500     */
501    @ServiceThreadOnly
502    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
503        assertRunOnServiceThread();
504        mCecController.sendCommand(command, callback);
505    }
506
507    @ServiceThreadOnly
508    void sendCecCommand(HdmiCecMessage command) {
509        assertRunOnServiceThread();
510        mCecController.sendCommand(command, null);
511    }
512
513    @ServiceThreadOnly
514    boolean handleCecCommand(HdmiCecMessage message) {
515        assertRunOnServiceThread();
516        if (!mMessageValidator.isValid(message)) {
517            return false;
518        }
519        return dispatchMessageToLocalDevice(message);
520    }
521
522    void setAudioReturnChannel(boolean enabled) {
523        mCecController.setAudioReturnChannel(enabled);
524    }
525
526    @ServiceThreadOnly
527    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
528        assertRunOnServiceThread();
529        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
530            if (device.dispatchMessage(message)
531                    && message.getDestination() != Constants.ADDR_BROADCAST) {
532                return true;
533            }
534        }
535
536        if (message.getDestination() != Constants.ADDR_BROADCAST) {
537            Slog.w(TAG, "Unhandled cec command:" + message);
538        }
539        return false;
540    }
541
542    /**
543     * Called when a new hotplug event is issued.
544     *
545     * @param portNo hdmi port number where hot plug event issued.
546     * @param connected whether to be plugged in or not
547     */
548    @ServiceThreadOnly
549    void onHotplug(int portNo, boolean connected) {
550        assertRunOnServiceThread();
551        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
552            device.onHotplug(portNo, connected);
553        }
554        announceHotplugEvent(portNo, connected);
555    }
556
557    /**
558     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
559     * devices.
560     *
561     * @param callback an interface used to get a list of all remote devices' address
562     * @param sourceAddress a logical address of source device where sends polling message
563     * @param pickStrategy strategy how to pick polling candidates
564     * @param retryCount the number of retry used to send polling message to remote devices
565     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
566     */
567    @ServiceThreadOnly
568    void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy,
569            int retryCount) {
570        assertRunOnServiceThread();
571        mCecController.pollDevices(callback, sourceAddress, checkPollStrategy(pickStrategy),
572                retryCount);
573    }
574
575    private int checkPollStrategy(int pickStrategy) {
576        int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK;
577        if (strategy == 0) {
578            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
579        }
580        int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK;
581        if (iterationStrategy == 0) {
582            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
583        }
584        return strategy | iterationStrategy;
585    }
586
587    List<HdmiCecLocalDevice> getAllLocalDevices() {
588        assertRunOnServiceThread();
589        return mCecController.getLocalDeviceList();
590    }
591
592    Object getServiceLock() {
593        return mLock;
594    }
595
596    void setAudioStatus(boolean mute, int volume) {
597        AudioManager audioManager = getAudioManager();
598        boolean muted = audioManager.isStreamMute(AudioManager.STREAM_MUSIC);
599        if (mute) {
600            if (!muted) {
601                audioManager.setStreamMute(AudioManager.STREAM_MUSIC, true);
602            }
603        } else {
604            if (muted) {
605                audioManager.setStreamMute(AudioManager.STREAM_MUSIC, false);
606            }
607            // FLAG_HDMI_SYSTEM_AUDIO_VOLUME prevents audio manager from announcing
608            // volume change notification back to hdmi control service.
609            audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume,
610                    AudioManager.FLAG_SHOW_UI |
611                    AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME);
612        }
613    }
614
615    void announceSystemAudioModeChange(boolean enabled) {
616        for (IHdmiSystemAudioModeChangeListener listener : mSystemAudioModeChangeListeners) {
617            invokeSystemAudioModeChange(listener, enabled);
618        }
619    }
620
621    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
622        // TODO: find better name instead of model name.
623        String displayName = Build.MODEL;
624        return new HdmiCecDeviceInfo(logicalAddress,
625                getPhysicalAddress(), pathToPortId(getPhysicalAddress()), deviceType,
626                getVendorId(), displayName);
627    }
628
629    // Record class that monitors the event of the caller of being killed. Used to clean up
630    // the listener list and record list accordingly.
631    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
632        private final IHdmiHotplugEventListener mListener;
633
634        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
635            mListener = listener;
636        }
637
638        @Override
639        public void binderDied() {
640            synchronized (mLock) {
641                mHotplugEventListenerRecords.remove(this);
642                mHotplugEventListeners.remove(mListener);
643            }
644        }
645    }
646
647    private final class DeviceEventListenerRecord implements IBinder.DeathRecipient {
648        private final IHdmiDeviceEventListener mListener;
649
650        public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) {
651            mListener = listener;
652        }
653
654        @Override
655        public void binderDied() {
656            synchronized (mLock) {
657                mDeviceEventListenerRecords.remove(this);
658                mDeviceEventListeners.remove(mListener);
659            }
660        }
661    }
662
663    private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient {
664        private final IHdmiSystemAudioModeChangeListener mListener;
665
666        public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) {
667            mListener = listener;
668        }
669
670        @Override
671        public void binderDied() {
672            synchronized (mLock) {
673                mSystemAudioModeChangeListenerRecords.remove(this);
674                mSystemAudioModeChangeListeners.remove(mListener);
675            }
676        }
677    }
678
679    class VendorCommandListenerRecord implements IBinder.DeathRecipient {
680        private final IHdmiVendorCommandListener mListener;
681        private final int mDeviceType;
682
683        public VendorCommandListenerRecord(IHdmiVendorCommandListener listener, int deviceType) {
684            mListener = listener;
685            mDeviceType = deviceType;
686        }
687
688        @Override
689        public void binderDied() {
690            synchronized (mLock) {
691                mVendorCommandListenerRecords.remove(this);
692            }
693        }
694    }
695
696    private class HdmiRecordRequestListenerRecord implements IBinder.DeathRecipient {
697        @Override
698        public void binderDied() {
699            synchronized (mLock) {
700                mRecordRequestListener = null;
701            }
702        }
703    }
704
705    private void enforceAccessPermission() {
706        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
707    }
708
709    private final class BinderService extends IHdmiControlService.Stub {
710        @Override
711        public int[] getSupportedTypes() {
712            enforceAccessPermission();
713            // mLocalDevices is an unmodifiable list - no lock necesary.
714            int[] localDevices = new int[mLocalDevices.size()];
715            for (int i = 0; i < localDevices.length; ++i) {
716                localDevices[i] = mLocalDevices.get(i);
717            }
718            return localDevices;
719        }
720
721        @Override
722        public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) {
723            enforceAccessPermission();
724            runOnServiceThread(new Runnable() {
725                @Override
726                public void run() {
727                    HdmiCecLocalDeviceTv tv = tv();
728                    if (tv == null) {
729                        Slog.w(TAG, "Local tv device not available");
730                        invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
731                        return;
732                    }
733                    tv.deviceSelect(logicalAddress, callback);
734                }
735            });
736        }
737
738        @Override
739        public void portSelect(final int portId, final IHdmiControlCallback callback) {
740            enforceAccessPermission();
741            runOnServiceThread(new Runnable() {
742                @Override
743                public void run() {
744                    HdmiCecLocalDeviceTv tv = tv();
745                    if (tv == null) {
746                        Slog.w(TAG, "Local tv device not available");
747                        invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
748                        return;
749                    }
750                    tv.doManualPortSwitching(portId, callback);
751                }
752            });
753        }
754
755        @Override
756        public void sendKeyEvent(final int deviceType, final int keyCode, final boolean isPressed) {
757            enforceAccessPermission();
758            runOnServiceThread(new Runnable() {
759                @Override
760                public void run() {
761                    HdmiCecLocalDevice localDevice = mCecController.getLocalDevice(deviceType);
762                    if (localDevice == null) {
763                        Slog.w(TAG, "Local device not available");
764                        return;
765                    }
766                    localDevice.sendKeyEvent(keyCode, isPressed);
767                }
768            });
769        }
770
771        @Override
772        public void oneTouchPlay(final IHdmiControlCallback callback) {
773            enforceAccessPermission();
774            runOnServiceThread(new Runnable() {
775                @Override
776                public void run() {
777                    HdmiControlService.this.oneTouchPlay(callback);
778                }
779            });
780        }
781
782        @Override
783        public void queryDisplayStatus(final IHdmiControlCallback callback) {
784            enforceAccessPermission();
785            runOnServiceThread(new Runnable() {
786                @Override
787                public void run() {
788                    HdmiControlService.this.queryDisplayStatus(callback);
789                }
790            });
791        }
792
793        @Override
794        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
795            enforceAccessPermission();
796            runOnServiceThread(new Runnable() {
797                @Override
798                public void run() {
799                    HdmiControlService.this.addHotplugEventListener(listener);
800                }
801            });
802        }
803
804        @Override
805        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
806            enforceAccessPermission();
807            runOnServiceThread(new Runnable() {
808                @Override
809                public void run() {
810                    HdmiControlService.this.removeHotplugEventListener(listener);
811                }
812            });
813        }
814
815        @Override
816        public void addDeviceEventListener(final IHdmiDeviceEventListener listener) {
817            enforceAccessPermission();
818            runOnServiceThread(new Runnable() {
819                @Override
820                public void run() {
821                    HdmiControlService.this.addDeviceEventListener(listener);
822                }
823            });
824        }
825
826        @Override
827        public List<HdmiPortInfo> getPortInfo() {
828            enforceAccessPermission();
829            return mPortInfo;
830        }
831
832        @Override
833        public boolean canChangeSystemAudioMode() {
834            enforceAccessPermission();
835            HdmiCecLocalDeviceTv tv = tv();
836            if (tv == null) {
837                return false;
838            }
839            return tv.hasSystemAudioDevice();
840        }
841
842        @Override
843        public boolean getSystemAudioMode() {
844            enforceAccessPermission();
845            HdmiCecLocalDeviceTv tv = tv();
846            if (tv == null) {
847                return false;
848            }
849            return tv.isSystemAudioActivated();
850        }
851
852        @Override
853        public void setSystemAudioMode(final boolean enabled, final IHdmiControlCallback callback) {
854            enforceAccessPermission();
855            runOnServiceThread(new Runnable() {
856                @Override
857                public void run() {
858                    HdmiCecLocalDeviceTv tv = tv();
859                    if (tv == null) {
860                        Slog.w(TAG, "Local tv device not available");
861                        invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
862                        return;
863                    }
864                    tv.changeSystemAudioMode(enabled, callback);
865                }
866            });
867        }
868
869        @Override
870        public void addSystemAudioModeChangeListener(
871                final IHdmiSystemAudioModeChangeListener listener) {
872            enforceAccessPermission();
873            HdmiControlService.this.addSystemAudioModeChangeListner(listener);
874        }
875
876        @Override
877        public void removeSystemAudioModeChangeListener(
878                final IHdmiSystemAudioModeChangeListener listener) {
879            enforceAccessPermission();
880            HdmiControlService.this.removeSystemAudioModeChangeListener(listener);
881        }
882
883        @Override
884        public void setInputChangeListener(final IHdmiInputChangeListener listener) {
885            enforceAccessPermission();
886            HdmiControlService.this.setInputChangeListener(listener);
887        }
888
889        @Override
890        public List<HdmiCecDeviceInfo> getInputDevices() {
891            enforceAccessPermission();
892            // No need to hold the lock for obtaining TV device as the local device instance
893            // is preserved while the HDMI control is enabled.
894            HdmiCecLocalDeviceTv tv = tv();
895            if (tv == null) {
896                return Collections.emptyList();
897            }
898            return tv.getSafeExternalInputs();
899        }
900
901        @Override
902        public void setControlEnabled(final boolean enabled) {
903            enforceAccessPermission();
904            runOnServiceThread(new Runnable() {
905                @Override
906                public void run() {
907                    handleHdmiControlStatusChanged(enabled);
908
909                }
910            });
911        }
912
913        @Override
914        public void setSystemAudioVolume(final int oldIndex, final int newIndex,
915                final int maxIndex) {
916            enforceAccessPermission();
917            runOnServiceThread(new Runnable() {
918                @Override
919                public void run() {
920                    HdmiCecLocalDeviceTv tv = tv();
921                    if (tv == null) {
922                        Slog.w(TAG, "Local tv device not available");
923                        return;
924                    }
925                    tv.changeVolume(oldIndex, newIndex - oldIndex, maxIndex);
926                }
927            });
928        }
929
930        @Override
931        public void setSystemAudioMute(final boolean mute) {
932            enforceAccessPermission();
933            runOnServiceThread(new Runnable() {
934                @Override
935                public void run() {
936                    HdmiCecLocalDeviceTv tv = tv();
937                    if (tv == null) {
938                        Slog.w(TAG, "Local tv device not available");
939                        return;
940                    }
941                    tv.changeMute(mute);
942                }
943            });
944        }
945
946        @Override
947        public void setArcMode(final boolean enabled) {
948            enforceAccessPermission();
949            runOnServiceThread(new Runnable() {
950                @Override
951                public void run() {
952                    HdmiCecLocalDeviceTv tv = tv();
953                    if (tv == null) {
954                        Slog.w(TAG, "Local tv device not available to change arc mode.");
955                        return;
956                    }
957                }
958            });
959        }
960
961        @Override
962        public void setOption(final int key, final int value) {
963            enforceAccessPermission();
964            if (!isTvDevice()) {
965                return;
966            }
967            switch (key) {
968                case HdmiTvClient.OPTION_CEC_AUTO_WAKEUP:
969                    mCecController.setOption(key, value);
970                    break;
971                case HdmiTvClient.OPTION_CEC_AUTO_DEVICE_OFF:
972                    // No need to pass this option to HAL.
973                    tv().setAutoDeviceOff(value == HdmiTvClient.ENABLED);
974                    break;
975                case HdmiTvClient.OPTION_MHL_INPUT_SWITCHING:  // Fall through
976                case HdmiTvClient.OPTION_MHL_POWER_CHARGE:
977                    if (mMhlController != null) {
978                        mMhlController.setOption(key, value);
979                    }
980                    break;
981            }
982        }
983
984        private boolean isTvDevice() {
985            return tv() != null;
986        }
987
988        @Override
989        public void setProhibitMode(final boolean enabled) {
990            enforceAccessPermission();
991            if (!isTvDevice()) {
992                return;
993            }
994            HdmiControlService.this.setProhibitMode(enabled);
995        }
996
997        @Override
998        public void addVendorCommandListener(final IHdmiVendorCommandListener listener,
999                final int deviceType) {
1000            enforceAccessPermission();
1001            runOnServiceThread(new Runnable() {
1002                @Override
1003                public void run() {
1004                    HdmiControlService.this.addVendorCommandListener(listener, deviceType);
1005                }
1006            });
1007        }
1008
1009        @Override
1010        public void sendVendorCommand(final int deviceType, final int targetAddress,
1011                final byte[] params, final boolean hasVendorId) {
1012            enforceAccessPermission();
1013            runOnServiceThread(new Runnable() {
1014                @Override
1015                public void run() {
1016                    HdmiCecLocalDevice device = mCecController.getLocalDevice(deviceType);
1017                    if (device == null) {
1018                        Slog.w(TAG, "Local device not available");
1019                        return;
1020                    }
1021                    if (hasVendorId) {
1022                        sendCecCommand(HdmiCecMessageBuilder.buildVendorCommandWithId(
1023                                device.getDeviceInfo().getLogicalAddress(), targetAddress,
1024                                getVendorId(), params));
1025                    } else {
1026                        sendCecCommand(HdmiCecMessageBuilder.buildVendorCommand(
1027                                device.getDeviceInfo().getLogicalAddress(), targetAddress, params));
1028                    }
1029                }
1030            });
1031         }
1032
1033        @Override
1034        public void setOneTouchRecordRequestListener(IHdmiRecordRequestListener listener) {
1035            HdmiControlService.this.setOneTouchRecordRequestListener(listener);
1036        }
1037
1038        @Override
1039        public void startOneTouchRecord(final int recorderAddress, final byte[] recordSource) {
1040            runOnServiceThread(new Runnable() {
1041                @Override
1042                public void run() {
1043                    if (!isTvDevice()) {
1044                        Slog.w(TAG, "No TV is available.");
1045                        return;
1046                    }
1047                    tv().startOneTouchRecord(recorderAddress, recordSource);
1048                }
1049            });
1050        }
1051
1052        @Override
1053        public void stopOneTouchRecord(final int recorderAddress) {
1054            runOnServiceThread(new Runnable() {
1055                @Override
1056                public void run() {
1057                    if (!isTvDevice()) {
1058                        Slog.w(TAG, "No TV is available.");
1059                        return;
1060                    }
1061                    tv().stopOneTouchRecord(recorderAddress);
1062                }
1063            });
1064        }
1065
1066        @Override
1067        public void startTimerRecording(final int recorderAddress, final int sourceType,
1068                final byte[] recordSource) {
1069            runOnServiceThread(new Runnable() {
1070                @Override
1071                public void run() {
1072                    if (!isTvDevice()) {
1073                        Slog.w(TAG, "No TV is available.");
1074                        return;
1075                    }
1076                    tv().startTimerRecording(recorderAddress, sourceType, recordSource);
1077                }
1078            });
1079        }
1080
1081        @Override
1082        public void clearTimerRecording(final int recorderAddress, final int sourceType,
1083                final byte[] recordSource) {
1084            runOnServiceThread(new Runnable() {
1085                @Override
1086                public void run() {
1087                    if (!isTvDevice()) {
1088                        Slog.w(TAG, "No TV is available.");
1089                        return;
1090                    }
1091                    tv().clearTimerRecording(recorderAddress, sourceType, recordSource);
1092                }
1093            });
1094        }
1095    }
1096
1097    @ServiceThreadOnly
1098    private void oneTouchPlay(final IHdmiControlCallback callback) {
1099        assertRunOnServiceThread();
1100        HdmiCecLocalDevicePlayback source = playback();
1101        if (source == null) {
1102            Slog.w(TAG, "Local playback device not available");
1103            invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
1104            return;
1105        }
1106        source.oneTouchPlay(callback);
1107    }
1108
1109    @ServiceThreadOnly
1110    private void queryDisplayStatus(final IHdmiControlCallback callback) {
1111        assertRunOnServiceThread();
1112        HdmiCecLocalDevicePlayback source = playback();
1113        if (source == null) {
1114            Slog.w(TAG, "Local playback device not available");
1115            invokeCallback(callback, HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE);
1116            return;
1117        }
1118        source.queryDisplayStatus(callback);
1119    }
1120
1121    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
1122        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
1123        try {
1124            listener.asBinder().linkToDeath(record, 0);
1125        } catch (RemoteException e) {
1126            Slog.w(TAG, "Listener already died");
1127            return;
1128        }
1129        synchronized (mLock) {
1130            mHotplugEventListenerRecords.add(record);
1131            mHotplugEventListeners.add(listener);
1132        }
1133    }
1134
1135    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
1136        synchronized (mLock) {
1137            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
1138                if (record.mListener.asBinder() == listener.asBinder()) {
1139                    listener.asBinder().unlinkToDeath(record, 0);
1140                    mHotplugEventListenerRecords.remove(record);
1141                    break;
1142                }
1143            }
1144            mHotplugEventListeners.remove(listener);
1145        }
1146    }
1147
1148    private void addDeviceEventListener(IHdmiDeviceEventListener listener) {
1149        DeviceEventListenerRecord record = new DeviceEventListenerRecord(listener);
1150        try {
1151            listener.asBinder().linkToDeath(record, 0);
1152        } catch (RemoteException e) {
1153            Slog.w(TAG, "Listener already died");
1154            return;
1155        }
1156        synchronized (mLock) {
1157            mDeviceEventListeners.add(listener);
1158            mDeviceEventListenerRecords.add(record);
1159        }
1160    }
1161
1162    void invokeDeviceEventListeners(HdmiCecDeviceInfo device, boolean activated) {
1163        synchronized (mLock) {
1164            for (IHdmiDeviceEventListener listener : mDeviceEventListeners) {
1165                try {
1166                    listener.onStatusChanged(device, activated);
1167                } catch (RemoteException e) {
1168                    Slog.e(TAG, "Failed to report device event:" + e);
1169                }
1170            }
1171        }
1172    }
1173
1174    private void addSystemAudioModeChangeListner(IHdmiSystemAudioModeChangeListener listener) {
1175        SystemAudioModeChangeListenerRecord record = new SystemAudioModeChangeListenerRecord(
1176                listener);
1177        try {
1178            listener.asBinder().linkToDeath(record, 0);
1179        } catch (RemoteException e) {
1180            Slog.w(TAG, "Listener already died");
1181            return;
1182        }
1183        synchronized (mLock) {
1184            mSystemAudioModeChangeListeners.add(listener);
1185            mSystemAudioModeChangeListenerRecords.add(record);
1186        }
1187    }
1188
1189    private void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {
1190        synchronized (mLock) {
1191            for (SystemAudioModeChangeListenerRecord record :
1192                    mSystemAudioModeChangeListenerRecords) {
1193                if (record.mListener.asBinder() == listener) {
1194                    listener.asBinder().unlinkToDeath(record, 0);
1195                    mSystemAudioModeChangeListenerRecords.remove(record);
1196                    break;
1197                }
1198            }
1199            mSystemAudioModeChangeListeners.remove(listener);
1200        }
1201    }
1202
1203    private final class InputChangeListenerRecord implements IBinder.DeathRecipient {
1204        @Override
1205        public void binderDied() {
1206            synchronized (mLock) {
1207                mInputChangeListener = null;
1208            }
1209        }
1210    }
1211
1212    private void setInputChangeListener(IHdmiInputChangeListener listener) {
1213        synchronized (mLock) {
1214            mInputChangeListenerRecord = new InputChangeListenerRecord();
1215            try {
1216                listener.asBinder().linkToDeath(mInputChangeListenerRecord, 0);
1217            } catch (RemoteException e) {
1218                Slog.w(TAG, "Listener already died");
1219                return;
1220            }
1221            mInputChangeListener = listener;
1222        }
1223    }
1224
1225    void invokeInputChangeListener(int activeAddress) {
1226        synchronized (mLock) {
1227            if (mInputChangeListener != null) {
1228                HdmiCecDeviceInfo activeSource = getDeviceInfo(activeAddress);
1229                try {
1230                    mInputChangeListener.onChanged(activeSource);
1231                } catch (RemoteException e) {
1232                    Slog.w(TAG, "Exception thrown by IHdmiInputChangeListener: " + e);
1233                }
1234            }
1235        }
1236    }
1237
1238    private void setOneTouchRecordRequestListener(IHdmiRecordRequestListener listener) {
1239        synchronized (mLock) {
1240            mRecordRequestListenerRecord = new HdmiRecordRequestListenerRecord();
1241            try {
1242                listener.asBinder().linkToDeath(mRecordRequestListenerRecord, 0);
1243            } catch (RemoteException e) {
1244                Slog.w(TAG, "Listener already died", e);
1245                return;
1246            }
1247            mRecordRequestListener = listener;
1248        }
1249    }
1250
1251    byte[] invokeRecordRequestListener(int recorderAddress) {
1252        synchronized (mLock) {
1253            try {
1254                if (mRecordRequestListener != null) {
1255                    return mRecordRequestListener.onRecordRequestReceived(recorderAddress);
1256                }
1257            } catch (RemoteException e) {
1258                Slog.w(TAG, "Failed to start record.", e);
1259            }
1260            return EmptyArray.BYTE;
1261        }
1262    }
1263
1264    private void invokeCallback(IHdmiControlCallback callback, int result) {
1265        try {
1266            callback.onComplete(result);
1267        } catch (RemoteException e) {
1268            Slog.e(TAG, "Invoking callback failed:" + e);
1269        }
1270    }
1271
1272    private void invokeSystemAudioModeChange(IHdmiSystemAudioModeChangeListener listener,
1273            boolean enabled) {
1274        try {
1275            listener.onStatusChanged(enabled);
1276        } catch (RemoteException e) {
1277            Slog.e(TAG, "Invoking callback failed:" + e);
1278        }
1279    }
1280
1281    private void announceHotplugEvent(int portId, boolean connected) {
1282        HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected);
1283        synchronized (mLock) {
1284            for (IHdmiHotplugEventListener listener : mHotplugEventListeners) {
1285                invokeHotplugEventListenerLocked(listener, event);
1286            }
1287        }
1288    }
1289
1290    private void invokeHotplugEventListenerLocked(IHdmiHotplugEventListener listener,
1291            HdmiHotplugEvent event) {
1292        try {
1293            listener.onReceived(event);
1294        } catch (RemoteException e) {
1295            Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e);
1296        }
1297    }
1298
1299    private static boolean hasSameTopPort(int path1, int path2) {
1300        return (path1 & Constants.ROUTING_PATH_TOP_MASK)
1301                == (path2 & Constants.ROUTING_PATH_TOP_MASK);
1302    }
1303
1304    private HdmiCecLocalDeviceTv tv() {
1305        return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCecDeviceInfo.DEVICE_TV);
1306    }
1307
1308    private HdmiCecLocalDevicePlayback playback() {
1309        return (HdmiCecLocalDevicePlayback)
1310                mCecController.getLocalDevice(HdmiCecDeviceInfo.DEVICE_PLAYBACK);
1311    }
1312
1313    AudioManager getAudioManager() {
1314        return (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
1315    }
1316
1317    boolean isControlEnabled() {
1318        synchronized (mLock) {
1319            return mHdmiControlEnabled;
1320        }
1321    }
1322
1323    int getPowerStatus() {
1324        return mPowerStatus;
1325    }
1326
1327    boolean isPowerOnOrTransient() {
1328        return mPowerStatus == HdmiControlManager.POWER_STATUS_ON
1329                || mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
1330    }
1331
1332    boolean isPowerStandbyOrTransient() {
1333        return mPowerStatus == HdmiControlManager.POWER_STATUS_STANDBY
1334                || mPowerStatus == HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY;
1335    }
1336
1337    boolean isPowerStandby() {
1338        return mPowerStatus == HdmiControlManager.POWER_STATUS_STANDBY;
1339    }
1340
1341    @ServiceThreadOnly
1342    void wakeUp() {
1343        assertRunOnServiceThread();
1344        PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
1345        pm.wakeUp(SystemClock.uptimeMillis());
1346        // PowerManger will send the broadcast Intent.ACTION_SCREEN_ON and after this gets
1347        // the intent, the sequence will continue at onWakeUp().
1348    }
1349
1350    @ServiceThreadOnly
1351    void standby() {
1352        assertRunOnServiceThread();
1353        mStandbyMessageReceived = true;
1354        PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
1355        pm.goToSleep(SystemClock.uptimeMillis());
1356        // PowerManger will send the broadcast Intent.ACTION_SCREEN_OFF and after this gets
1357        // the intent, the sequence will continue at onStandby().
1358    }
1359
1360    @ServiceThreadOnly
1361    private void onWakeUp() {
1362        assertRunOnServiceThread();
1363        mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
1364        if (mCecController != null) {
1365            if (mHdmiControlEnabled) {
1366                initializeCec(true);
1367            }
1368        } else {
1369            Slog.i(TAG, "Device does not support HDMI-CEC.");
1370        }
1371        // TODO: Initialize MHL local devices.
1372    }
1373
1374    @ServiceThreadOnly
1375    private void onStandby() {
1376        assertRunOnServiceThread();
1377        mPowerStatus = HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY;
1378
1379        final List<HdmiCecLocalDevice> devices = getAllLocalDevices();
1380        disableDevices(new PendingActionClearedCallback() {
1381            @Override
1382            public void onCleared(HdmiCecLocalDevice device) {
1383                Slog.v(TAG, "On standby-action cleared:" + device.mDeviceType);
1384                devices.remove(device);
1385                if (devices.isEmpty()) {
1386                    clearLocalDevices();
1387                    onStandbyCompleted();
1388                }
1389            }
1390        });
1391    }
1392
1393    private void disableDevices(PendingActionClearedCallback callback) {
1394        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
1395            device.disableDevice(mStandbyMessageReceived, callback);
1396        }
1397    }
1398
1399    @ServiceThreadOnly
1400    private void clearLocalDevices() {
1401        assertRunOnServiceThread();
1402        if (mCecController == null) {
1403            return;
1404        }
1405        mCecController.clearLogicalAddress();
1406        mCecController.clearLocalDevices();
1407    }
1408
1409    @ServiceThreadOnly
1410    private void onStandbyCompleted() {
1411        assertRunOnServiceThread();
1412        Slog.v(TAG, "onStandbyCompleted");
1413
1414        if (mPowerStatus != HdmiControlManager.POWER_STATUS_TRANSIENT_TO_STANDBY) {
1415            return;
1416        }
1417        mPowerStatus = HdmiControlManager.POWER_STATUS_STANDBY;
1418        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
1419            device.onStandby(mStandbyMessageReceived);
1420        }
1421        mStandbyMessageReceived = false;
1422        mCecController.setOption(HdmiTvClient.OPTION_CEC_SERVICE_CONTROL, HdmiTvClient.DISABLED);
1423    }
1424
1425    private void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) {
1426        VendorCommandListenerRecord record = new VendorCommandListenerRecord(listener, deviceType);
1427        try {
1428            listener.asBinder().linkToDeath(record, 0);
1429        } catch (RemoteException e) {
1430            Slog.w(TAG, "Listener already died");
1431            return;
1432        }
1433        synchronized (mLock) {
1434            mVendorCommandListenerRecords.add(record);
1435        }
1436    }
1437
1438    void invokeVendorCommandListeners(int deviceType, int srcAddress, byte[] params,
1439            boolean hasVendorId) {
1440        synchronized (mLock) {
1441            for (VendorCommandListenerRecord record : mVendorCommandListenerRecords) {
1442                if (record.mDeviceType != deviceType) {
1443                    continue;
1444                }
1445                try {
1446                    record.mListener.onReceived(srcAddress, params, hasVendorId);
1447                } catch (RemoteException e) {
1448                    Slog.e(TAG, "Failed to notify vendor command reception", e);
1449                }
1450            }
1451        }
1452    }
1453
1454    boolean isProhibitMode() {
1455        synchronized (mLock) {
1456            return mProhibitMode;
1457        }
1458    }
1459
1460    void setProhibitMode(boolean enabled) {
1461        synchronized (mLock) {
1462            mProhibitMode = enabled;
1463        }
1464    }
1465
1466    @ServiceThreadOnly
1467    private void handleHdmiControlStatusChanged(boolean enabled) {
1468        assertRunOnServiceThread();
1469
1470        int value = enabled ? HdmiTvClient.ENABLED : HdmiTvClient.DISABLED;
1471        mCecController.setOption(HdmiTvClient.OPTION_CEC_ENABLE, value);
1472        if (mMhlController != null) {
1473            mMhlController.setOption(HdmiTvClient.OPTION_MHL_ENABLE, value);
1474        }
1475
1476        synchronized (mLock) {
1477            mHdmiControlEnabled = enabled;
1478        }
1479
1480        if (enabled) {
1481            initializeCec(false);
1482        } else {
1483            disableDevices(new PendingActionClearedCallback() {
1484                @Override
1485                public void onCleared(HdmiCecLocalDevice device) {
1486                    assertRunOnServiceThread();
1487                    clearLocalDevices();
1488                }
1489            });
1490        }
1491    }
1492}
1493