HdmiControlService.java revision 9d499bfe4a52068fd0c25b3cce34bd5e445e0f96
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    /**
182     * Remove the given {@link FeatureAction} object from the action queue.
183     *
184     * @param action {@link FeatureAction} to remove
185     */
186    void removeAction(final FeatureAction action) {
187        runOnServiceThread(new Runnable() {
188            @Override
189            public void run() {
190                mActions.remove(action);
191            }
192        });
193    }
194
195    // Remove all actions matched with the given Class type.
196    private <T extends FeatureAction> void removeAction(final Class<T> clazz) {
197        runOnServiceThread(new Runnable() {
198            @Override
199            public void run() {
200                Iterator<FeatureAction> iter = mActions.iterator();
201                while (iter.hasNext()) {
202                    FeatureAction action = iter.next();
203                    if (action.getClass().equals(clazz)) {
204                        action.clear();
205                        mActions.remove(action);
206                    }
207                }
208            }
209        });
210    }
211
212    private void runOnServiceThread(Runnable runnable) {
213        mHandler.post(runnable);
214    }
215
216    /**
217     * Change ARC status into the given {@code enabled} status.
218     *
219     * @return {@code true} if ARC was in "Enabled" status
220     */
221    boolean setArcStatus(boolean enabled) {
222        boolean oldStatus = mArcStatusEnabled;
223        // 1. Enable/disable ARC circuit.
224        // TODO: call set_audio_return_channel of hal interface.
225
226        // 2. Update arc status;
227        mArcStatusEnabled = enabled;
228        return oldStatus;
229    }
230
231    /**
232     * Transmit a CEC command to CEC bus.
233     *
234     * @param command CEC command to send out
235     * @param callback interface used to the result of send command
236     */
237    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
238        mCecController.sendCommand(command, callback);
239    }
240
241    void sendCecCommand(HdmiCecMessage command) {
242        mCecController.sendCommand(command, null);
243    }
244
245    /**
246     * Add a new {@link HdmiCecDeviceInfo} to controller.
247     *
248     * @param deviceInfo new device information object to add
249     */
250    void addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
251        // TODO: Implement this.
252    }
253
254    boolean handleCecCommand(HdmiCecMessage message) {
255        // Commands that queries system information replies directly instead
256        // of creating FeatureAction because they are state-less.
257        switch (message.getOpcode()) {
258            case HdmiCec.MESSAGE_GET_MENU_LANGUAGE:
259                handleGetMenuLanguage(message);
260                return true;
261            case HdmiCec.MESSAGE_GIVE_OSD_NAME:
262                handleGiveOsdName(message);
263                return true;
264            case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS:
265                handleGivePhysicalAddress(message);
266                return true;
267            case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID:
268                handleGiveDeviceVendorId(message);
269                return true;
270            case HdmiCec.MESSAGE_GET_CEC_VERSION:
271                handleGetCecVersion(message);
272                return true;
273            case HdmiCec.MESSAGE_INITIATE_ARC:
274                handleInitiateArc(message);
275                return true;
276            case HdmiCec.MESSAGE_TERMINATE_ARC:
277                handleTerminateArc(message);
278                return true;
279            // TODO: Add remaining system information query such as
280            // <Give Device Power Status> and <Request Active Source> handler.
281            default:
282                Slog.w(TAG, "Unsupported cec command:" + message.toString());
283                return false;
284        }
285    }
286
287    /**
288     * Called when a new hotplug event is issued.
289     *
290     * @param port hdmi port number where hot plug event issued.
291     * @param connected whether to be plugged in or not
292     */
293    void onHotplug(int portNo, boolean connected) {
294        // TODO: Start "RequestArcInitiationAction" if ARC port.
295    }
296
297    /**
298     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
299     * devices.
300     *
301     * @param callback an interface used to get a list of all remote devices' address
302     * @param retryCount the number of retry used to send polling message to remote devices
303     */
304    void pollDevices(DevicePollingCallback callback, int retryCount) {
305        mCecController.pollDevices(callback, retryCount);
306    }
307
308    private void handleInitiateArc(HdmiCecMessage message){
309        // In case where <Initiate Arc> is started by <Request ARC Initiation>
310        // need to clean up RequestArcInitiationAction.
311        removeAction(RequestArcInitiationAction.class);
312        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
313                message.getDestination(), message.getSource(), true);
314        addAndStartAction(action);
315    }
316
317    private void handleTerminateArc(HdmiCecMessage message) {
318        // In case where <Terminate Arc> is started by <Request ARC Termination>
319        // need to clean up RequestArcInitiationAction.
320        // TODO: check conditions of power status by calling is_connected api
321        // to be added soon.
322        removeAction(RequestArcTerminationAction.class);
323        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
324                message.getDestination(), message.getSource(), false);
325        addAndStartAction(action);
326    }
327
328    private void handleGetCecVersion(HdmiCecMessage message) {
329        int version = mCecController.getVersion();
330        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
331                message.getSource(),
332                version);
333        sendCecCommand(cecMessage);
334    }
335
336    private void handleGiveDeviceVendorId(HdmiCecMessage message) {
337        int vendorId = mCecController.getVendorId();
338        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
339                message.getDestination(), vendorId);
340        sendCecCommand(cecMessage);
341    }
342
343    private void handleGivePhysicalAddress(HdmiCecMessage message) {
344        int physicalAddress = mCecController.getPhysicalAddress();
345        int deviceType = HdmiCec.getTypeFromAddress(message.getDestination());
346        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
347                message.getDestination(), physicalAddress, deviceType);
348        sendCecCommand(cecMessage);
349    }
350
351    private void handleGiveOsdName(HdmiCecMessage message) {
352        // TODO: read device name from settings or property.
353        String name = HdmiCec.getDefaultDeviceName(message.getDestination());
354        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
355                message.getDestination(), message.getSource(), name);
356        if (cecMessage != null) {
357            sendCecCommand(cecMessage);
358        } else {
359            Slog.w(TAG, "Failed to build <Get Osd Name>:" + name);
360        }
361    }
362
363    private void handleGetMenuLanguage(HdmiCecMessage message) {
364        // Only 0 (TV), 14 (specific use) can answer.
365        if (message.getDestination() != HdmiCec.ADDR_TV
366                && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) {
367            Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
368            sendCecCommand(
369                    HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(),
370                            message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE,
371                            HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE));
372            return;
373        }
374
375        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
376                message.getDestination(),
377                Locale.getDefault().getISO3Language());
378        // TODO: figure out how to handle failed to get language code.
379        if (command != null) {
380            sendCecCommand(command);
381        } else {
382            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
383        }
384    }
385
386    // Record class that monitors the event of the caller of being killed. Used to clean up
387    // the listener list and record list accordingly.
388    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
389        private final IHdmiHotplugEventListener mListener;
390
391        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
392            mListener = listener;
393        }
394
395        @Override
396        public void binderDied() {
397            synchronized (mLock) {
398                mHotplugEventListenerRecords.remove(this);
399                mHotplugEventListeners.remove(mListener);
400            }
401        }
402    }
403
404    private void enforceAccessPermission() {
405        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
406    }
407
408    private final class BinderService extends IHdmiControlService.Stub {
409        @Override
410        public int[] getSupportedTypes() {
411            enforceAccessPermission();
412            synchronized (mLock) {
413                return mLocalDevices;
414            }
415        }
416
417        @Override
418        public void oneTouchPlay(IHdmiControlCallback callback) {
419            enforceAccessPermission();
420            // TODO: Post a message for HdmiControlService#oneTouchPlay()
421        }
422
423        @Override
424        public void queryDisplayStatus(IHdmiControlCallback callback) {
425            enforceAccessPermission();
426            // TODO: Post a message for HdmiControlService#queryDisplayStatus()
427        }
428
429        @Override
430        public void addHotplugEventListener(IHdmiHotplugEventListener listener) {
431            enforceAccessPermission();
432            // TODO: Post a message for HdmiControlService#addHotplugEventListener()
433        }
434
435        @Override
436        public void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
437            enforceAccessPermission();
438            // TODO: Post a message for HdmiControlService#removeHotplugEventListener()
439        }
440    }
441
442    private void oneTouchPlay(IHdmiControlCallback callback) {
443        // TODO: Create a new action
444    }
445
446    private void queryDisplayStatus(IHdmiControlCallback callback) {
447        // TODO: Create a new action
448    }
449
450    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
451        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
452        try {
453            listener.asBinder().linkToDeath(record, 0);
454        } catch (RemoteException e) {
455            Slog.w(TAG, "Listener already died");
456            return;
457        }
458        synchronized (mLock) {
459            mHotplugEventListenerRecords.add(record);
460            mHotplugEventListeners.add(listener);
461        }
462    }
463
464    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
465        synchronized (mLock) {
466            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
467                if (record.mListener.asBinder() == listener.asBinder()) {
468                    listener.asBinder().unlinkToDeath(record, 0);
469                    mHotplugEventListenerRecords.remove(record);
470                    break;
471                }
472            }
473            mHotplugEventListeners.remove(listener);
474        }
475    }
476}
477