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