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