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