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