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