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