HdmiControlService.java revision 8692fc68a413c7aca75f094bb02bcbabcc277c73
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;
36import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
37
38import java.util.ArrayList;
39import java.util.Iterator;
40import java.util.LinkedList;
41import java.util.List;
42import java.util.Locale;
43
44/**
45 * Provides a service for sending and processing HDMI control messages,
46 * HDMI-CEC and MHL control command, and providing the information on both standard.
47 */
48public final class HdmiControlService extends SystemService {
49    private static final String TAG = "HdmiControlService";
50
51    // TODO: Rename the permission to HDMI_CONTROL.
52    private static final String PERMISSION = "android.permission.HDMI_CEC";
53
54    static final int SEND_RESULT_SUCCESS = 0;
55    static final int SEND_RESULT_NAK = -1;
56    static final int SEND_RESULT_FAILURE = -2;
57
58    /**
59     * Interface to report send result.
60     */
61    interface SendMessageCallback {
62        /**
63         * Called when {@link HdmiControlService#sendCecCommand} is completed.
64         *
65         * @param error result of send request.
66         * @see {@link #SEND_RESULT_SUCCESS}
67         * @see {@link #SEND_RESULT_NAK}
68         * @see {@link #SEND_RESULT_FAILURE}
69         */
70        void onSendCompleted(int error);
71    }
72
73    /**
74     * Interface to get a list of available logical devices.
75     */
76    interface DevicePollingCallback {
77        /**
78         * Called when device polling is finished.
79         *
80         * @param ackedAddress a list of logical addresses of available devices
81         */
82        void onPollingFinished(List<Integer> ackedAddress);
83    }
84
85    // A thread to handle synchronous IO of CEC and MHL control service.
86    // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms)
87    // and sparse call it shares a thread to handle IO operations.
88    private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread");
89
90    // A collection of FeatureAction.
91    // Note that access to this collection should happen in service thread.
92    private final LinkedList<FeatureAction> mActions = new LinkedList<>();
93
94    // Used to synchronize the access to the service.
95    private final Object mLock = new Object();
96
97    // Type of logical devices hosted in the system.
98    @GuardedBy("mLock")
99    private final int[] mLocalDevices;
100
101    // List of listeners registered by callers that want to get notified of
102    // hotplug events.
103    private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>();
104
105    // List of records for hotplug event listener to handle the the caller killed in action.
106    private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
107            new ArrayList<>();
108
109    @Nullable
110    private HdmiCecController mCecController;
111
112    @Nullable
113    private HdmiMhlController mMhlController;
114
115    // Whether ARC is "enabled" or not.
116    // TODO: it may need to hold lock if it's accessed from others.
117    private boolean mArcStatusEnabled = false;
118
119    // Handler running on service thread. It's used to run a task in service thread.
120    private Handler mHandler = new Handler();
121
122    public HdmiControlService(Context context) {
123        super(context);
124        mLocalDevices = getContext().getResources().getIntArray(
125                com.android.internal.R.array.config_hdmiCecLogicalDeviceType);
126    }
127
128    @Override
129    public void onStart() {
130        mIoThread.start();
131        mCecController = HdmiCecController.create(this);
132        if (mCecController != null) {
133            mCecController.initializeLocalDevices(mLocalDevices);
134        } else {
135            Slog.i(TAG, "Device does not support HDMI-CEC.");
136        }
137
138        mMhlController = HdmiMhlController.create(this);
139        if (mMhlController == null) {
140            Slog.i(TAG, "Device does not support MHL-control.");
141        }
142
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                return dispatchMessageToAction(message);
293        }
294    }
295
296    /**
297     * Called when a new hotplug event is issued.
298     *
299     * @param port hdmi port number where hot plug event issued.
300     * @param connected whether to be plugged in or not
301     */
302    void onHotplug(int portNo, boolean connected) {
303        // TODO: Start "RequestArcInitiationAction" if ARC port.
304    }
305
306    /**
307     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
308     * devices.
309     *
310     * @param callback an interface used to get a list of all remote devices' address
311     * @param retryCount the number of retry used to send polling message to remote devices
312     */
313    void pollDevices(DevicePollingCallback callback, int retryCount) {
314        mCecController.pollDevices(callback, retryCount);
315    }
316
317    private void handleInitiateArc(HdmiCecMessage message){
318        // In case where <Initiate Arc> is started by <Request ARC Initiation>
319        // need to clean up RequestArcInitiationAction.
320        removeAction(RequestArcInitiationAction.class);
321        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
322                message.getDestination(), message.getSource(), true);
323        addAndStartAction(action);
324    }
325
326    private void handleTerminateArc(HdmiCecMessage message) {
327        // In case where <Terminate Arc> is started by <Request ARC Termination>
328        // need to clean up RequestArcInitiationAction.
329        // TODO: check conditions of power status by calling is_connected api
330        // to be added soon.
331        removeAction(RequestArcTerminationAction.class);
332        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
333                message.getDestination(), message.getSource(), false);
334        addAndStartAction(action);
335    }
336
337    private void handleGetCecVersion(HdmiCecMessage message) {
338        int version = mCecController.getVersion();
339        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
340                message.getSource(),
341                version);
342        sendCecCommand(cecMessage);
343    }
344
345    private void handleGiveDeviceVendorId(HdmiCecMessage message) {
346        int vendorId = mCecController.getVendorId();
347        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
348                message.getDestination(), vendorId);
349        sendCecCommand(cecMessage);
350    }
351
352    private void handleGivePhysicalAddress(HdmiCecMessage message) {
353        int physicalAddress = mCecController.getPhysicalAddress();
354        int deviceType = HdmiCec.getTypeFromAddress(message.getDestination());
355        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
356                message.getDestination(), physicalAddress, deviceType);
357        sendCecCommand(cecMessage);
358    }
359
360    private void handleGiveOsdName(HdmiCecMessage message) {
361        // TODO: read device name from settings or property.
362        String name = HdmiCec.getDefaultDeviceName(message.getDestination());
363        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
364                message.getDestination(), message.getSource(), name);
365        if (cecMessage != null) {
366            sendCecCommand(cecMessage);
367        } else {
368            Slog.w(TAG, "Failed to build <Get Osd Name>:" + name);
369        }
370    }
371
372    private void handleGetMenuLanguage(HdmiCecMessage message) {
373        // Only 0 (TV), 14 (specific use) can answer.
374        if (message.getDestination() != HdmiCec.ADDR_TV
375                && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) {
376            Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
377            sendCecCommand(
378                    HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(),
379                            message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE,
380                            HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE));
381            return;
382        }
383
384        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
385                message.getDestination(),
386                Locale.getDefault().getISO3Language());
387        // TODO: figure out how to handle failed to get language code.
388        if (command != null) {
389            sendCecCommand(command);
390        } else {
391            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
392        }
393    }
394
395    private boolean dispatchMessageToAction(HdmiCecMessage message) {
396        for (FeatureAction action : mActions) {
397            if (action.processCommand(message)) {
398                return true;
399            }
400        }
401        Slog.w(TAG, "Unsupported cec command:" + message);
402        return false;
403    }
404
405    // Record class that monitors the event of the caller of being killed. Used to clean up
406    // the listener list and record list accordingly.
407    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
408        private final IHdmiHotplugEventListener mListener;
409
410        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
411            mListener = listener;
412        }
413
414        @Override
415        public void binderDied() {
416            synchronized (mLock) {
417                mHotplugEventListenerRecords.remove(this);
418                mHotplugEventListeners.remove(mListener);
419            }
420        }
421    }
422
423    void addCecDevice(HdmiCecDeviceInfo info) {
424        mCecController.addDeviceInfo(info);
425    }
426
427    // Launch device discovery sequence.
428    // It starts with clearing the existing device info list.
429    // Note that it assumes that logical address of all local devices is already allocated.
430    void launchDeviceDiscovery() {
431        // At first, clear all existing device infos.
432        mCecController.clearDeviceInfoList();
433
434        // TODO: check whether TV is one of local devices.
435        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, HdmiCec.ADDR_TV,
436                new DeviceDiscoveryCallback() {
437                    @Override
438                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
439                        for (HdmiCecDeviceInfo info : deviceInfos) {
440                            mCecController.addDeviceInfo(info);
441                        }
442
443                        // Add device info of all local devices.
444                        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
445                            mCecController.addDeviceInfo(device.getDeviceInfo());
446                        }
447
448                        // TODO: start hot-plug detection sequence here.
449                        // addAndStartAction(new HotplugDetectionAction());
450                    }
451                });
452        addAndStartAction(action);
453    }
454
455    private void enforceAccessPermission() {
456        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
457    }
458
459    private final class BinderService extends IHdmiControlService.Stub {
460        @Override
461        public int[] getSupportedTypes() {
462            enforceAccessPermission();
463            synchronized (mLock) {
464                return mLocalDevices;
465            }
466        }
467
468        @Override
469        public void oneTouchPlay(final IHdmiControlCallback callback) {
470            enforceAccessPermission();
471            runOnServiceThread(new Runnable() {
472                @Override
473                public void run() {
474                    HdmiControlService.this.oneTouchPlay(callback);
475                }
476            });
477        }
478
479        @Override
480        public void queryDisplayStatus(final IHdmiControlCallback callback) {
481            enforceAccessPermission();
482            runOnServiceThread(new Runnable() {
483                @Override
484                public void run() {
485                    HdmiControlService.this.queryDisplayStatus(callback);
486                }
487            });
488        }
489
490        @Override
491        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
492            enforceAccessPermission();
493            runOnServiceThread(new Runnable() {
494                @Override
495                public void run() {
496                    HdmiControlService.this.addHotplugEventListener(listener);
497                }
498            });
499        }
500
501        @Override
502        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
503            enforceAccessPermission();
504            runOnServiceThread(new Runnable() {
505                @Override
506                public void run() {
507                    HdmiControlService.this.removeHotplugEventListener(listener);
508                }
509            });
510        }
511    }
512
513    private void oneTouchPlay(IHdmiControlCallback callback) {
514        if (hasAction(OneTouchPlayAction.class)) {
515            Slog.w(TAG, "oneTouchPlay already in progress");
516            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
517            return;
518        }
519        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
520        if (source == null) {
521            Slog.w(TAG, "Local playback device not available");
522            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
523            return;
524        }
525        // TODO: Consider the case of multiple TV sets. For now we always direct the command
526        //       to the primary one.
527        OneTouchPlayAction action = OneTouchPlayAction.create(this,
528                source.getDeviceInfo().getLogicalAddress(),
529                source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback);
530        if (action == null) {
531            Slog.w(TAG, "Cannot initiate oneTouchPlay");
532            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
533            return;
534        }
535        addAndStartAction(action);
536    }
537
538    private void queryDisplayStatus(IHdmiControlCallback callback) {
539        if (hasAction(DevicePowerStatusAction.class)) {
540            Slog.w(TAG, "queryDisplayStatus already in progress");
541            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
542            return;
543        }
544        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
545        if (source == null) {
546            Slog.w(TAG, "Local playback device not available");
547            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
548            return;
549        }
550        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
551                source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback);
552        if (action == null) {
553            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
554            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
555            return;
556        }
557        addAndStartAction(action);
558    }
559
560    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
561        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
562        try {
563            listener.asBinder().linkToDeath(record, 0);
564        } catch (RemoteException e) {
565            Slog.w(TAG, "Listener already died");
566            return;
567        }
568        synchronized (mLock) {
569            mHotplugEventListenerRecords.add(record);
570            mHotplugEventListeners.add(listener);
571        }
572    }
573
574    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
575        synchronized (mLock) {
576            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
577                if (record.mListener.asBinder() == listener.asBinder()) {
578                    listener.asBinder().unlinkToDeath(record, 0);
579                    mHotplugEventListenerRecords.remove(record);
580                    break;
581                }
582            }
583            mHotplugEventListeners.remove(listener);
584        }
585    }
586
587    private void invokeCallback(IHdmiControlCallback callback, int result) {
588        try {
589            callback.onComplete(result);
590        } catch (RemoteException e) {
591            Slog.e(TAG, "Invoking callback failed:" + e);
592        }
593    }
594}
595