HdmiControlService.java revision 79c58a4b97f27ede6a1b680d2fece9c2a0edf7b7
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.Context;
21import android.hardware.hdmi.HdmiCec;
22import android.hardware.hdmi.HdmiCecDeviceInfo;
23import android.hardware.hdmi.HdmiCecMessage;
24import android.hardware.hdmi.HdmiHotplugEvent;
25import android.hardware.hdmi.HdmiPortInfo;
26import android.hardware.hdmi.IHdmiControlCallback;
27import android.hardware.hdmi.IHdmiControlService;
28import android.hardware.hdmi.IHdmiHotplugEventListener;
29import android.os.Build;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.IBinder;
33import android.os.Looper;
34import android.os.RemoteException;
35import android.util.Slog;
36import android.util.SparseArray;
37import android.util.SparseIntArray;
38
39import com.android.server.SystemService;
40import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback;
41
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.List;
45
46/**
47 * Provides a service for sending and processing HDMI control messages,
48 * HDMI-CEC and MHL control command, and providing the information on both standard.
49 */
50public final class HdmiControlService extends SystemService {
51    private static final String TAG = "HdmiControlService";
52
53    // TODO: Rename the permission to HDMI_CONTROL.
54    private static final String PERMISSION = "android.permission.HDMI_CEC";
55
56    static final int SEND_RESULT_SUCCESS = 0;
57    static final int SEND_RESULT_NAK = -1;
58    static final int SEND_RESULT_FAILURE = -2;
59
60    static final int POLL_STRATEGY_MASK = 0x3;  // first and second bit.
61    static final int POLL_STRATEGY_REMOTES_DEVICES = 0x1;
62    static final int POLL_STRATEGY_SYSTEM_AUDIO = 0x2;
63
64    static final int POLL_ITERATION_STRATEGY_MASK = 0x30000;  // first and second bit.
65    static final int POLL_ITERATION_IN_ORDER = 0x10000;
66    static final int POLL_ITERATION_REVERSE_ORDER = 0x20000;
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    // A thread to handle synchronous IO of CEC and MHL control service.
96    // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms)
97    // and sparse call it shares a thread to handle IO operations.
98    private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread");
99
100    // Used to synchronize the access to the service.
101    private final Object mLock = new Object();
102
103    // Type of logical devices hosted in the system. Stored in the unmodifiable list.
104    private final List<Integer> mLocalDevices;
105
106    // List of listeners registered by callers that want to get notified of
107    // hotplug events.
108    private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>();
109
110    // List of records for hotplug event listener to handle the the caller killed in action.
111    private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
112            new ArrayList<>();
113
114    // Handler running on service thread. It's used to run a task in service thread.
115    private final Handler mHandler = new Handler();
116
117    @Nullable
118    private HdmiCecController mCecController;
119
120    @Nullable
121    private HdmiMhlController mMhlController;
122
123    // HDMI port information. Stored in the unmodifiable list to keep the static information
124    // from being modified.
125    private List<HdmiPortInfo> mPortInfo;
126
127    public HdmiControlService(Context context) {
128        super(context);
129        mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray(
130                com.android.internal.R.array.config_hdmiCecLogicalDeviceType));
131    }
132
133    @Override
134    public void onStart() {
135        mIoThread.start();
136        mCecController = HdmiCecController.create(this);
137
138        if (mCecController != null) {
139            initializeLocalDevices(mLocalDevices);
140        } else {
141            Slog.i(TAG, "Device does not support HDMI-CEC.");
142        }
143
144        mMhlController = HdmiMhlController.create(this);
145        if (mMhlController == null) {
146            Slog.i(TAG, "Device does not support MHL-control.");
147        }
148        mPortInfo = initPortInfo();
149        publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService());
150
151        // TODO: Read the preference for SystemAudioMode and initialize mSystemAudioMode and
152        // start to monitor the preference value and invoke SystemAudioActionFromTv if needed.
153    }
154
155    private void initializeLocalDevices(final List<Integer> deviceTypes) {
156        // A container for [Logical Address, Local device info].
157        final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>();
158        final SparseIntArray finished = new SparseIntArray();
159        for (int type : deviceTypes) {
160            final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type);
161            localDevice.init();
162            mCecController.allocateLogicalAddress(type,
163                    localDevice.getPreferredAddress(), new AllocateAddressCallback() {
164                @Override
165                public void onAllocated(int deviceType, int logicalAddress) {
166                    if (logicalAddress == HdmiCec.ADDR_UNREGISTERED) {
167                        Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]");
168                    } else {
169                        HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType);
170                        localDevice.setDeviceInfo(deviceInfo);
171                        mCecController.addLocalDevice(deviceType, localDevice);
172                        mCecController.addLogicalAddress(logicalAddress);
173                        devices.append(logicalAddress, localDevice);
174                    }
175                    finished.append(deviceType, logicalAddress);
176
177                    // Once finish address allocation for all devices, notify
178                    // it to each device.
179                    if (deviceTypes.size() == finished.size()) {
180                        notifyAddressAllocated(devices);
181                    }
182                }
183            });
184        }
185    }
186
187    private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices) {
188        for (int i = 0; i < devices.size(); ++i) {
189            int address = devices.keyAt(i);
190            HdmiCecLocalDevice device = devices.valueAt(i);
191            device.onAddressAllocated(address);
192        }
193    }
194
195    // Initialize HDMI port information. Combine the information from CEC and MHL HAL and
196    // keep them in one place.
197    private List<HdmiPortInfo> initPortInfo() {
198        HdmiPortInfo[] cecPortInfo = null;
199
200        // CEC HAL provides majority of the info while MHL does only MHL support flag for
201        // each port. Return empty array if CEC HAL didn't provide the info.
202        if (mCecController != null) {
203            cecPortInfo = mCecController.getPortInfos();
204        }
205        if (cecPortInfo == null) {
206            return Collections.emptyList();
207        }
208
209        HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0];
210        if (mMhlController != null) {
211            // TODO: Implement plumbing logic to get MHL port information.
212            // mhlPortInfo = mMhlController.getPortInfos();
213        }
214
215        // Use the id (port number) to find the matched info between CEC and MHL to combine them
216        // into one. Leave the field `mhlSupported` to false if matched MHL entry is not found.
217        ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length);
218        for (int i = 0; i < cecPortInfo.length; ++i) {
219            HdmiPortInfo cec = cecPortInfo[i];
220            int id = cec.getId();
221            boolean mhlInfoFound = false;
222            for (HdmiPortInfo mhl : mhlPortInfo) {
223                if (id == mhl.getId()) {
224                    result.add(new HdmiPortInfo(id, cec.getType(), cec.getAddress(),
225                            cec.isCecSupported(), mhl.isMhlSupported(), cec.isArcSupported()));
226                    mhlInfoFound = true;
227                    break;
228                }
229            }
230            if (!mhlInfoFound) {
231                result.add(cec);
232            }
233        }
234
235        return Collections.unmodifiableList(result);
236    }
237
238    /**
239     * Returns HDMI port information for the given port id.
240     *
241     * @param portId HDMI port id
242     * @return {@link HdmiPortInfo} for the given port
243     */
244    HdmiPortInfo getPortInfo(int portId) {
245        // mPortInfo is an unmodifiable list and the only reference to its inner list.
246        // No lock is necessary.
247        for (HdmiPortInfo info : mPortInfo) {
248            if (portId == info.getId()) {
249                return info;
250            }
251        }
252        return null;
253    }
254
255    /**
256     * Returns the routing path (physical address) of the HDMI port for the given
257     * port id.
258     */
259    int portIdToPath(int portId) {
260        HdmiPortInfo portInfo = getPortInfo(portId);
261        if (portInfo == null) {
262            Slog.e(TAG, "Cannot find the port info: " + portId);
263            return HdmiConstants.INVALID_PHYSICAL_ADDRESS;
264        }
265        return portInfo.getAddress();
266    }
267
268    /**
269     * Returns the id of HDMI port located at the top of the hierarchy of
270     * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance,
271     * the port id to be returned is the ID associated with the port address
272     * 0x1000 (1.0.0.0) which is the topmost path of the given routing path.
273     */
274    int pathToPortId(int path) {
275        int portAddress = path & HdmiConstants.ROUTING_PATH_TOP_MASK;
276        for (HdmiPortInfo info : mPortInfo) {
277            if (portAddress == info.getAddress()) {
278                return info.getId();
279            }
280        }
281        return HdmiConstants.INVALID_PORT_ID;
282    }
283
284    /**
285     * Returns {@link Looper} for IO operation.
286     *
287     * <p>Declared as package-private.
288     */
289    Looper getIoLooper() {
290        return mIoThread.getLooper();
291    }
292
293    /**
294     * Returns {@link Looper} of main thread. Use this {@link Looper} instance
295     * for tasks that are running on main service thread.
296     *
297     * <p>Declared as package-private.
298     */
299    Looper getServiceLooper() {
300        return mHandler.getLooper();
301    }
302
303    /**
304     * Returns physical address of the device.
305     */
306    int getPhysicalAddress() {
307        return mCecController.getPhysicalAddress();
308    }
309
310    /**
311     * Returns vendor id of CEC service.
312     */
313    int getVendorId() {
314        return mCecController.getVendorId();
315    }
316
317    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
318        assertRunOnServiceThread();
319        HdmiCecLocalDeviceTv tv = tv();
320        if (tv == null) {
321            return null;
322        }
323        return tv.getDeviceInfo(logicalAddress);
324    }
325
326    /**
327     * Returns version of CEC.
328     */
329    int getCecVersion() {
330        return mCecController.getVersion();
331    }
332
333    /**
334     * Whether a device of the specified physical address is connected to ARC enabled port.
335     */
336    boolean isConnectedToArcPort(int physicalAddress) {
337        for (HdmiPortInfo portInfo : mPortInfo) {
338            if (hasSameTopPort(portInfo.getAddress(), physicalAddress)
339                    && portInfo.isArcSupported()) {
340                return true;
341            }
342        }
343        return false;
344    }
345
346    void runOnServiceThread(Runnable runnable) {
347        mHandler.post(runnable);
348    }
349
350    void runOnServiceThreadAtFrontOfQueue(Runnable runnable) {
351        mHandler.postAtFrontOfQueue(runnable);
352    }
353
354    private void assertRunOnServiceThread() {
355        if (Looper.myLooper() != mHandler.getLooper()) {
356            throw new IllegalStateException("Should run on service thread.");
357        }
358    }
359
360    /**
361     * Transmit a CEC command to CEC bus.
362     *
363     * @param command CEC command to send out
364     * @param callback interface used to the result of send command
365     */
366    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
367        mCecController.sendCommand(command, callback);
368    }
369
370    void sendCecCommand(HdmiCecMessage command) {
371        mCecController.sendCommand(command, null);
372    }
373
374    boolean handleCecCommand(HdmiCecMessage message) {
375        return dispatchMessageToLocalDevice(message);
376    }
377
378    void setAudioReturnChannel(boolean enabled) {
379        mCecController.setAudioReturnChannel(enabled);
380    }
381
382    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
383        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
384            if (device.dispatchMessage(message)
385                    && message.getDestination() != HdmiCec.ADDR_BROADCAST) {
386                return true;
387            }
388        }
389
390        Slog.w(TAG, "Unhandled cec command:" + message);
391        return false;
392    }
393
394    /**
395     * Called when a new hotplug event is issued.
396     *
397     * @param portNo hdmi port number where hot plug event issued.
398     * @param connected whether to be plugged in or not
399     */
400    void onHotplug(int portNo, boolean connected) {
401        assertRunOnServiceThread();
402
403        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
404            device.onHotplug(portNo, connected);
405        }
406
407        announceHotplugEvent(portNo, connected);
408    }
409
410    /**
411     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
412     * devices.
413     *
414     * @param callback an interface used to get a list of all remote devices' address
415     * @param pickStrategy strategy how to pick polling candidates
416     * @param retryCount the number of retry used to send polling message to remote devices
417     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
418     */
419    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
420        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
421    }
422
423    private int checkPollStrategy(int pickStrategy) {
424        int strategy = pickStrategy & POLL_STRATEGY_MASK;
425        if (strategy == 0) {
426            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
427        }
428        int iterationStrategy = pickStrategy & POLL_ITERATION_STRATEGY_MASK;
429        if (iterationStrategy == 0) {
430            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
431        }
432        return strategy | iterationStrategy;
433    }
434
435    List<HdmiCecLocalDevice> getAllLocalDevices() {
436        assertRunOnServiceThread();
437        return mCecController.getLocalDeviceList();
438    }
439
440    Object getServiceLock() {
441        return mLock;
442    }
443
444    void setAudioStatus(boolean mute, int volume) {
445        // TODO: Hook up with AudioManager.
446    }
447
448    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
449        // TODO: find better name instead of model name.
450        String displayName = Build.MODEL;
451        return new HdmiCecDeviceInfo(logicalAddress,
452                getPhysicalAddress(), deviceType, getVendorId(), displayName);
453    }
454
455    // Record class that monitors the event of the caller of being killed. Used to clean up
456    // the listener list and record list accordingly.
457    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
458        private final IHdmiHotplugEventListener mListener;
459
460        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
461            mListener = listener;
462        }
463
464        @Override
465        public void binderDied() {
466            synchronized (mLock) {
467                mHotplugEventListenerRecords.remove(this);
468                mHotplugEventListeners.remove(mListener);
469            }
470        }
471    }
472
473    private void enforceAccessPermission() {
474        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
475    }
476
477    private final class BinderService extends IHdmiControlService.Stub {
478        @Override
479        public int[] getSupportedTypes() {
480            enforceAccessPermission();
481            // mLocalDevices is an unmodifiable list - no lock necesary.
482            int[] localDevices = new int[mLocalDevices.size()];
483            for (int i = 0; i < localDevices.length; ++i) {
484                localDevices[i] = mLocalDevices.get(i);
485            }
486            return localDevices;
487        }
488
489        @Override
490        public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) {
491            enforceAccessPermission();
492            runOnServiceThread(new Runnable() {
493                @Override
494                public void run() {
495                    HdmiCecLocalDeviceTv tv = tv();
496                    if (tv == null) {
497                        Slog.w(TAG, "Local playback device not available");
498                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
499                        return;
500                    }
501                    tv.deviceSelect(logicalAddress, callback);
502                }
503            });
504        }
505
506        @Override
507        public void oneTouchPlay(final IHdmiControlCallback callback) {
508            enforceAccessPermission();
509            runOnServiceThread(new Runnable() {
510                @Override
511                public void run() {
512                    HdmiControlService.this.oneTouchPlay(callback);
513                }
514            });
515        }
516
517        @Override
518        public void queryDisplayStatus(final IHdmiControlCallback callback) {
519            enforceAccessPermission();
520            runOnServiceThread(new Runnable() {
521                @Override
522                public void run() {
523                    HdmiControlService.this.queryDisplayStatus(callback);
524                }
525            });
526        }
527
528        @Override
529        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
530            enforceAccessPermission();
531            runOnServiceThread(new Runnable() {
532                @Override
533                public void run() {
534                    HdmiControlService.this.addHotplugEventListener(listener);
535                }
536            });
537        }
538
539        @Override
540        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
541            enforceAccessPermission();
542            runOnServiceThread(new Runnable() {
543                @Override
544                public void run() {
545                    HdmiControlService.this.removeHotplugEventListener(listener);
546                }
547            });
548        }
549    }
550
551    private void oneTouchPlay(final IHdmiControlCallback callback) {
552        assertRunOnServiceThread();
553        HdmiCecLocalDevicePlayback source = playback();
554        if (source == null) {
555            Slog.w(TAG, "Local playback device not available");
556            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
557            return;
558        }
559        source.oneTouchPlay(callback);
560    }
561
562    private void queryDisplayStatus(final IHdmiControlCallback callback) {
563        assertRunOnServiceThread();
564        HdmiCecLocalDevicePlayback source = playback();
565        if (source == null) {
566            Slog.w(TAG, "Local playback device not available");
567            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
568            return;
569        }
570        source.queryDisplayStatus(callback);
571    }
572
573    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
574        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
575        try {
576            listener.asBinder().linkToDeath(record, 0);
577        } catch (RemoteException e) {
578            Slog.w(TAG, "Listener already died");
579            return;
580        }
581        synchronized (mLock) {
582            mHotplugEventListenerRecords.add(record);
583            mHotplugEventListeners.add(listener);
584        }
585    }
586
587    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
588        synchronized (mLock) {
589            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
590                if (record.mListener.asBinder() == listener.asBinder()) {
591                    listener.asBinder().unlinkToDeath(record, 0);
592                    mHotplugEventListenerRecords.remove(record);
593                    break;
594                }
595            }
596            mHotplugEventListeners.remove(listener);
597        }
598    }
599
600    private void invokeCallback(IHdmiControlCallback callback, int result) {
601        try {
602            callback.onComplete(result);
603        } catch (RemoteException e) {
604            Slog.e(TAG, "Invoking callback failed:" + e);
605        }
606    }
607
608    private void announceHotplugEvent(int portNo, boolean connected) {
609        HdmiHotplugEvent event = new HdmiHotplugEvent(portNo, connected);
610        synchronized (mLock) {
611            for (IHdmiHotplugEventListener listener : mHotplugEventListeners) {
612                invokeHotplugEventListener(listener, event);
613            }
614        }
615    }
616
617    private void invokeHotplugEventListener(IHdmiHotplugEventListener listener,
618            HdmiHotplugEvent event) {
619        try {
620            listener.onReceived(event);
621        } catch (RemoteException e) {
622            Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e);
623        }
624    }
625
626    private static boolean hasSameTopPort(int path1, int path2) {
627        return (path1 & HdmiConstants.ROUTING_PATH_TOP_MASK)
628                == (path2 & HdmiConstants.ROUTING_PATH_TOP_MASK);
629    }
630
631    private HdmiCecLocalDeviceTv tv() {
632        return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV);
633    }
634
635    private HdmiCecLocalDevicePlayback playback() {
636        return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
637    }
638}
639