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