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