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