HdmiControlService.java revision 7fe2ae0fe9c24f0a1a5ddf20850069b56af2c2fd
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.IHdmiControlCallback;
25import android.hardware.hdmi.IHdmiControlService;
26import android.hardware.hdmi.IHdmiHotplugEventListener;
27import android.os.Handler;
28import android.os.HandlerThread;
29import android.os.IBinder;
30import android.os.Looper;
31import android.os.RemoteException;
32import android.util.Slog;
33
34import com.android.internal.annotations.GuardedBy;
35import com.android.server.SystemService;
36
37import java.util.ArrayList;
38import java.util.Iterator;
39import java.util.LinkedList;
40import java.util.List;
41import java.util.Locale;
42
43/**
44 * Provides a service for sending and processing HDMI control messages,
45 * HDMI-CEC and MHL control command, and providing the information on both standard.
46 */
47public final class HdmiControlService extends SystemService {
48    private static final String TAG = "HdmiControlService";
49
50    // TODO: Rename the permission to HDMI_CONTROL.
51    private static final String PERMISSION = "android.permission.HDMI_CEC";
52
53    static final int SEND_RESULT_SUCCESS = 0;
54    static final int SEND_RESULT_NAK = -1;
55    static final int SEND_RESULT_FAILURE = -2;
56
57    /**
58     * Interface to report send result.
59     */
60    interface SendMessageCallback {
61        /**
62         * Called when {@link HdmiControlService#sendCecCommand} is completed.
63         *
64         * @param error result of send request.
65         * @see {@link #SEND_RESULT_SUCCESS}
66         * @see {@link #SEND_RESULT_NAK}
67         * @see {@link #SEND_RESULT_FAILURE}
68         */
69        void onSendCompleted(int error);
70    }
71
72    /**
73     * Interface to get a list of available logical devices.
74     */
75    interface DevicePollingCallback {
76        /**
77         * Called when device polling is finished.
78         *
79         * @param ackedAddress a list of logical addresses of available devices
80         */
81        void onPollingFinished(List<Integer> ackedAddress);
82    }
83
84    // A thread to handle synchronous IO of CEC and MHL control service.
85    // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms)
86    // and sparse call it shares a thread to handle IO operations.
87    private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread");
88
89    // A collection of FeatureAction.
90    // Note that access to this collection should happen in service thread.
91    private final LinkedList<FeatureAction> mActions = new LinkedList<>();
92
93    // Used to synchronize the access to the service.
94    private final Object mLock = new Object();
95
96    // Type of logical devices hosted in the system.
97    @GuardedBy("mLock")
98    private final int[] mLocalDevices;
99
100    // List of listeners registered by callers that want to get notified of
101    // hotplug events.
102    private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>();
103
104    // List of records for hotplug event listener to handle the the caller killed in action.
105    private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
106            new ArrayList<>();
107
108    @Nullable
109    private HdmiCecController mCecController;
110
111    @Nullable
112    private HdmiMhlController mMhlController;
113
114    // Whether ARC is "enabled" or not.
115    // TODO: it may need to hold lock if it's accessed from others.
116    private boolean mArcStatusEnabled = false;
117
118    // Handler running on service thread. It's used to run a task in service thread.
119    private Handler mHandler = new Handler();
120
121    public HdmiControlService(Context context) {
122        super(context);
123        mLocalDevices = getContext().getResources().getIntArray(
124                com.android.internal.R.array.config_hdmiCecLogicalDeviceType);
125    }
126
127    @Override
128    public void onStart() {
129        mIoThread.start();
130        mCecController = HdmiCecController.create(this);
131        if (mCecController != null) {
132            mCecController.initializeLocalDevices(mLocalDevices);
133        } else {
134            Slog.i(TAG, "Device does not support HDMI-CEC.");
135        }
136
137        mMhlController = HdmiMhlController.create(this);
138        if (mMhlController == null) {
139            Slog.i(TAG, "Device does not support MHL-control.");
140        }
141
142        // TODO: Publish the BinderService
143        // publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService());
144    }
145
146    /**
147     * Returns {@link Looper} for IO operation.
148     *
149     * <p>Declared as package-private.
150     */
151    Looper getIoLooper() {
152        return mIoThread.getLooper();
153    }
154
155    /**
156     * Returns {@link Looper} of main thread. Use this {@link Looper} instance
157     * for tasks that are running on main service thread.
158     *
159     * <p>Declared as package-private.
160     */
161    Looper getServiceLooper() {
162        return mHandler.getLooper();
163    }
164
165    /**
166     * Add and start a new {@link FeatureAction} to the action queue.
167     *
168     * @param action {@link FeatureAction} to add and start
169     */
170    void addAndStartAction(final FeatureAction action) {
171        // TODO: may need to check the number of stale actions.
172        runOnServiceThread(new Runnable() {
173            @Override
174            public void run() {
175                mActions.add(action);
176                action.start();
177            }
178        });
179    }
180
181    // See if we have an action of a given type in progress.
182    private <T extends FeatureAction> boolean hasAction(final Class<T> clazz) {
183        for (FeatureAction action : mActions) {
184            if (action.getClass().equals(clazz)) {
185                return true;
186            }
187        }
188        return false;
189    }
190
191    /**
192     * Remove the given {@link FeatureAction} object from the action queue.
193     *
194     * @param action {@link FeatureAction} to remove
195     */
196    void removeAction(final FeatureAction action) {
197        runOnServiceThread(new Runnable() {
198            @Override
199            public void run() {
200                mActions.remove(action);
201            }
202        });
203    }
204
205    // Remove all actions matched with the given Class type.
206    private <T extends FeatureAction> void removeAction(final Class<T> clazz) {
207        runOnServiceThread(new Runnable() {
208            @Override
209            public void run() {
210                Iterator<FeatureAction> iter = mActions.iterator();
211                while (iter.hasNext()) {
212                    FeatureAction action = iter.next();
213                    if (action.getClass().equals(clazz)) {
214                        action.clear();
215                        mActions.remove(action);
216                    }
217                }
218            }
219        });
220    }
221
222    private void runOnServiceThread(Runnable runnable) {
223        mHandler.post(runnable);
224    }
225
226    /**
227     * Change ARC status into the given {@code enabled} status.
228     *
229     * @return {@code true} if ARC was in "Enabled" status
230     */
231    boolean setArcStatus(boolean enabled) {
232        boolean oldStatus = mArcStatusEnabled;
233        // 1. Enable/disable ARC circuit.
234        // TODO: call set_audio_return_channel of hal interface.
235
236        // 2. Update arc status;
237        mArcStatusEnabled = enabled;
238        return oldStatus;
239    }
240
241    /**
242     * Transmit a CEC command to CEC bus.
243     *
244     * @param command CEC command to send out
245     * @param callback interface used to the result of send command
246     */
247    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
248        mCecController.sendCommand(command, callback);
249    }
250
251    void sendCecCommand(HdmiCecMessage command) {
252        mCecController.sendCommand(command, null);
253    }
254
255    /**
256     * Add a new {@link HdmiCecDeviceInfo} to controller.
257     *
258     * @param deviceInfo new device information object to add
259     */
260    void addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
261        // TODO: Implement this.
262    }
263
264    boolean handleCecCommand(HdmiCecMessage message) {
265        // Commands that queries system information replies directly instead
266        // of creating FeatureAction because they are state-less.
267        switch (message.getOpcode()) {
268            case HdmiCec.MESSAGE_GET_MENU_LANGUAGE:
269                handleGetMenuLanguage(message);
270                return true;
271            case HdmiCec.MESSAGE_GIVE_OSD_NAME:
272                handleGiveOsdName(message);
273                return true;
274            case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS:
275                handleGivePhysicalAddress(message);
276                return true;
277            case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID:
278                handleGiveDeviceVendorId(message);
279                return true;
280            case HdmiCec.MESSAGE_GET_CEC_VERSION:
281                handleGetCecVersion(message);
282                return true;
283            case HdmiCec.MESSAGE_INITIATE_ARC:
284                handleInitiateArc(message);
285                return true;
286            case HdmiCec.MESSAGE_TERMINATE_ARC:
287                handleTerminateArc(message);
288                return true;
289            // TODO: Add remaining system information query such as
290            // <Give Device Power Status> and <Request Active Source> handler.
291            default:
292                Slog.w(TAG, "Unsupported cec command:" + message.toString());
293                return false;
294        }
295    }
296
297    /**
298     * Called when a new hotplug event is issued.
299     *
300     * @param port hdmi port number where hot plug event issued.
301     * @param connected whether to be plugged in or not
302     */
303    void onHotplug(int portNo, boolean connected) {
304        // TODO: Start "RequestArcInitiationAction" if ARC port.
305    }
306
307    /**
308     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
309     * devices.
310     *
311     * @param callback an interface used to get a list of all remote devices' address
312     * @param retryCount the number of retry used to send polling message to remote devices
313     */
314    void pollDevices(DevicePollingCallback callback, int retryCount) {
315        mCecController.pollDevices(callback, retryCount);
316    }
317
318    private void handleInitiateArc(HdmiCecMessage message){
319        // In case where <Initiate Arc> is started by <Request ARC Initiation>
320        // need to clean up RequestArcInitiationAction.
321        removeAction(RequestArcInitiationAction.class);
322        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
323                message.getDestination(), message.getSource(), true);
324        addAndStartAction(action);
325    }
326
327    private void handleTerminateArc(HdmiCecMessage message) {
328        // In case where <Terminate Arc> is started by <Request ARC Termination>
329        // need to clean up RequestArcInitiationAction.
330        // TODO: check conditions of power status by calling is_connected api
331        // to be added soon.
332        removeAction(RequestArcTerminationAction.class);
333        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
334                message.getDestination(), message.getSource(), false);
335        addAndStartAction(action);
336    }
337
338    private void handleGetCecVersion(HdmiCecMessage message) {
339        int version = mCecController.getVersion();
340        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
341                message.getSource(),
342                version);
343        sendCecCommand(cecMessage);
344    }
345
346    private void handleGiveDeviceVendorId(HdmiCecMessage message) {
347        int vendorId = mCecController.getVendorId();
348        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
349                message.getDestination(), vendorId);
350        sendCecCommand(cecMessage);
351    }
352
353    private void handleGivePhysicalAddress(HdmiCecMessage message) {
354        int physicalAddress = mCecController.getPhysicalAddress();
355        int deviceType = HdmiCec.getTypeFromAddress(message.getDestination());
356        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
357                message.getDestination(), physicalAddress, deviceType);
358        sendCecCommand(cecMessage);
359    }
360
361    private void handleGiveOsdName(HdmiCecMessage message) {
362        // TODO: read device name from settings or property.
363        String name = HdmiCec.getDefaultDeviceName(message.getDestination());
364        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
365                message.getDestination(), message.getSource(), name);
366        if (cecMessage != null) {
367            sendCecCommand(cecMessage);
368        } else {
369            Slog.w(TAG, "Failed to build <Get Osd Name>:" + name);
370        }
371    }
372
373    private void handleGetMenuLanguage(HdmiCecMessage message) {
374        // Only 0 (TV), 14 (specific use) can answer.
375        if (message.getDestination() != HdmiCec.ADDR_TV
376                && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) {
377            Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
378            sendCecCommand(
379                    HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(),
380                            message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE,
381                            HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE));
382            return;
383        }
384
385        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
386                message.getDestination(),
387                Locale.getDefault().getISO3Language());
388        // TODO: figure out how to handle failed to get language code.
389        if (command != null) {
390            sendCecCommand(command);
391        } else {
392            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
393        }
394    }
395
396    // Record class that monitors the event of the caller of being killed. Used to clean up
397    // the listener list and record list accordingly.
398    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
399        private final IHdmiHotplugEventListener mListener;
400
401        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
402            mListener = listener;
403        }
404
405        @Override
406        public void binderDied() {
407            synchronized (mLock) {
408                mHotplugEventListenerRecords.remove(this);
409                mHotplugEventListeners.remove(mListener);
410            }
411        }
412    }
413
414    private void enforceAccessPermission() {
415        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
416    }
417
418    private final class BinderService extends IHdmiControlService.Stub {
419        @Override
420        public int[] getSupportedTypes() {
421            enforceAccessPermission();
422            synchronized (mLock) {
423                return mLocalDevices;
424            }
425        }
426
427        @Override
428        public void oneTouchPlay(final IHdmiControlCallback callback) {
429            enforceAccessPermission();
430            runOnServiceThread(new Runnable() {
431                @Override
432                public void run() {
433                    HdmiControlService.this.oneTouchPlay(callback);
434                }
435            });
436        }
437
438        @Override
439        public void queryDisplayStatus(final IHdmiControlCallback callback) {
440            enforceAccessPermission();
441            runOnServiceThread(new Runnable() {
442                @Override
443                public void run() {
444                    HdmiControlService.this.queryDisplayStatus(callback);
445                }
446            });
447        }
448
449        @Override
450        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
451            enforceAccessPermission();
452            runOnServiceThread(new Runnable() {
453                @Override
454                public void run() {
455                    HdmiControlService.this.addHotplugEventListener(listener);
456                }
457            });
458        }
459
460        @Override
461        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
462            enforceAccessPermission();
463            runOnServiceThread(new Runnable() {
464                @Override
465                public void run() {
466                    HdmiControlService.this.removeHotplugEventListener(listener);
467                }
468            });
469        }
470    }
471
472    private void oneTouchPlay(IHdmiControlCallback callback) {
473        if (hasAction(OneTouchPlayAction.class)) {
474            Slog.w(TAG, "oneTouchPlay already in progress");
475            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
476            return;
477        }
478        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
479        if (source == null) {
480            Slog.w(TAG, "Local playback device not available");
481            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
482            return;
483        }
484        // TODO: Consider the case of multiple TV sets. For now we always direct the command
485        //       to the primary one.
486        OneTouchPlayAction action = OneTouchPlayAction.create(this,
487                source.getDeviceInfo().getLogicalAddress(),
488                source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback);
489        if (action == null) {
490            Slog.w(TAG, "Cannot initiate oneTouchPlay");
491            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
492            return;
493        }
494        addAndStartAction(action);
495    }
496
497    private void queryDisplayStatus(IHdmiControlCallback callback) {
498        if (hasAction(DevicePowerStatusAction.class)) {
499            Slog.w(TAG, "queryDisplayStatus already in progress");
500            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
501            return;
502        }
503        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
504        if (source == null) {
505            Slog.w(TAG, "Local playback device not available");
506            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
507            return;
508        }
509        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
510                source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback);
511        if (action == null) {
512            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
513            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
514            return;
515        }
516        addAndStartAction(action);
517    }
518
519    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
520        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
521        try {
522            listener.asBinder().linkToDeath(record, 0);
523        } catch (RemoteException e) {
524            Slog.w(TAG, "Listener already died");
525            return;
526        }
527        synchronized (mLock) {
528            mHotplugEventListenerRecords.add(record);
529            mHotplugEventListeners.add(listener);
530        }
531    }
532
533    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
534        synchronized (mLock) {
535            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
536                if (record.mListener.asBinder() == listener.asBinder()) {
537                    listener.asBinder().unlinkToDeath(record, 0);
538                    mHotplugEventListenerRecords.remove(record);
539                    break;
540                }
541            }
542            mHotplugEventListeners.remove(listener);
543        }
544    }
545
546    private void invokeCallback(IHdmiControlCallback callback, int result) {
547        try {
548            callback.onComplete(result);
549        } catch (RemoteException e) {
550            Slog.e(TAG, "Invoking callback failed:" + e);
551        }
552    }
553}
554