HdmiCecController.java revision 3ee65720e91c7f92ad5a034d7052122a606aa8d5
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.hardware.hdmi.HdmiCec;
20import android.hardware.hdmi.HdmiCecDeviceInfo;
21import android.hardware.hdmi.HdmiCecMessage;
22import android.os.Handler;
23import android.os.Looper;
24import android.os.MessageQueue;
25import android.util.Slog;
26import android.util.SparseArray;
27
28import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
29
30import libcore.util.EmptyArray;
31
32import java.util.ArrayList;
33import java.util.List;
34
35/**
36 * Manages HDMI-CEC command and behaviors. It converts user's command into CEC command
37 * and pass it to CEC HAL so that it sends message to other device. For incoming
38 * message it translates the message and delegates it to proper module.
39 *
40 * <p>It should be careful to access member variables on IO thread because
41 * it can be accessed from system thread as well.
42 *
43 * <p>It can be created only by {@link HdmiCecController#create}
44 *
45 * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
46 */
47final class HdmiCecController {
48    private static final String TAG = "HdmiCecController";
49
50    /**
51     * Interface to report allocated logical address.
52     */
53    interface AllocateAddressCallback {
54        /**
55         * Called when a new logical address is allocated.
56         *
57         * @param deviceType requested device type to allocate logical address
58         * @param logicalAddress allocated logical address. If it is
59         *                       {@link HdmiCec#ADDR_UNREGISTERED}, it means that
60         *                       it failed to allocate logical address for the given device type
61         */
62        void onAllocated(int deviceType, int logicalAddress);
63    }
64
65    private static final byte[] EMPTY_BODY = EmptyArray.BYTE;
66
67    // A message to pass cec send command to IO looper.
68    private static final int MSG_SEND_CEC_COMMAND = 1;
69    // A message to delegate logical allocation to IO looper.
70    private static final int MSG_ALLOCATE_LOGICAL_ADDRESS = 2;
71
72    // Message types to handle incoming message in main service looper.
73    private final static int MSG_RECEIVE_CEC_COMMAND = 1;
74    // A message to report allocated logical address to main control looper.
75    private final static int MSG_REPORT_LOGICAL_ADDRESS = 2;
76
77    private static final int NUM_LOGICAL_ADDRESS = 16;
78
79    private static final int RETRY_COUNT_FOR_LOGICAL_ADDRESS_ALLOCATION = 3;
80
81    // Handler instance to process synchronous I/O (mainly send) message.
82    private Handler mIoHandler;
83
84    // Handler instance to process various messages coming from other CEC
85    // device or issued by internal state change.
86    private Handler mControlHandler;
87
88    // Stores the pointer to the native implementation of the service that
89    // interacts with HAL.
90    private volatile long mNativePtr;
91
92    private HdmiControlService mService;
93
94    // Map-like container of all cec devices including local ones.
95    // A logical address of device is used as key of container.
96    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
97
98    // Stores the local CEC devices in the system. Device type is used for key.
99    private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
100
101    // Private constructor.  Use HdmiCecController.create().
102    private HdmiCecController() {
103    }
104
105    /**
106     * A factory method to get {@link HdmiCecController}. If it fails to initialize
107     * inner device or has no device it will return {@code null}.
108     *
109     * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
110     * @param service {@link HdmiControlService} instance used to create internal handler
111     *                and to pass callback for incoming message or event.
112     * @return {@link HdmiCecController} if device is initialized successfully. Otherwise,
113     *         returns {@code null}.
114     */
115    static HdmiCecController create(HdmiControlService service) {
116        HdmiCecController controller = new HdmiCecController();
117        long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
118        if (nativePtr == 0L) {
119            controller = null;
120            return null;
121        }
122
123        controller.init(service, nativePtr);
124        return controller;
125    }
126
127    private void init(HdmiControlService service, long nativePtr) {
128        mService = service;
129        mIoHandler = new Handler(service.getServiceLooper());
130        mControlHandler = new Handler(service.getServiceLooper());
131        mNativePtr = nativePtr;
132    }
133
134    void addLocalDevice(int deviceType, HdmiCecLocalDevice device) {
135        mLocalDevices.put(deviceType, device);
136    }
137
138    /**
139     * Allocate a new logical address of the given device type. Allocated
140     * address will be reported through {@link AllocateAddressCallback}.
141     *
142     * <p> Declared as package-private, accessed by {@link HdmiControlService} only.
143     *
144     * @param deviceType type of device to used to determine logical address
145     * @param preferredAddress a logical address preferred to be allocated.
146     *                         If sets {@link HdmiCec#ADDR_UNREGISTERED}, scans
147     *                         the smallest logical address matched with the given device type.
148     *                         Otherwise, scan address will start from {@code preferredAddress}
149     * @param callback callback interface to report allocated logical address to caller
150     */
151    void allocateLogicalAddress(final int deviceType, final int preferredAddress,
152            final AllocateAddressCallback callback) {
153        assertRunOnServiceThread();
154
155        runOnIoThread(new Runnable() {
156            @Override
157            public void run() {
158                handleAllocateLogicalAddress(deviceType, preferredAddress, callback);
159            }
160        });
161    }
162
163    private void handleAllocateLogicalAddress(final int deviceType, int preferredAddress,
164            final AllocateAddressCallback callback) {
165        assertRunOnIoThread();
166        int startAddress = preferredAddress;
167        // If preferred address is "unregistered", start address will be the smallest
168        // address matched with the given device type.
169        if (preferredAddress == HdmiCec.ADDR_UNREGISTERED) {
170            for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
171                if (deviceType == HdmiCec.getTypeFromAddress(i)) {
172                    startAddress = i;
173                    break;
174                }
175            }
176        }
177
178        int logicalAddress = HdmiCec.ADDR_UNREGISTERED;
179        // Iterates all possible addresses which has the same device type.
180        for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
181            int curAddress = (startAddress + i) % NUM_LOGICAL_ADDRESS;
182            if (curAddress != HdmiCec.ADDR_UNREGISTERED
183                    && deviceType == HdmiCec.getTypeFromAddress(curAddress)) {
184                if (!sendPollMessage(curAddress, RETRY_COUNT_FOR_LOGICAL_ADDRESS_ALLOCATION)) {
185                    logicalAddress = curAddress;
186                    break;
187                }
188            }
189        }
190
191        final int assignedAddress = logicalAddress;
192        if (callback != null) {
193            runOnServiceThread(new Runnable() {
194                    @Override
195                public void run() {
196                    callback.onAllocated(deviceType, assignedAddress);
197                }
198            });
199        }
200    }
201
202    private static byte[] buildBody(int opcode, byte[] params) {
203        byte[] body = new byte[params.length + 1];
204        body[0] = (byte) opcode;
205        System.arraycopy(params, 0, body, 1, params.length);
206        return body;
207    }
208
209    /**
210     * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same
211     * logical address as new device info's.
212     *
213     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
214     *
215     * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added.
216     * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo}
217     *         that has the same logical address as new one has.
218     */
219    HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
220        assertRunOnServiceThread();
221        HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress());
222        if (oldDeviceInfo != null) {
223            removeDeviceInfo(deviceInfo.getLogicalAddress());
224        }
225        mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo);
226        return oldDeviceInfo;
227    }
228
229    /**
230     * Remove a device info corresponding to the given {@code logicalAddress}.
231     * It returns removed {@link HdmiCecDeviceInfo} if exists.
232     *
233     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
234     *
235     * @param logicalAddress logical address of device to be removed
236     * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null}
237     */
238    HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) {
239        assertRunOnServiceThread();
240        HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress);
241        if (deviceInfo != null) {
242            mDeviceInfos.remove(logicalAddress);
243        }
244        return deviceInfo;
245    }
246
247    /**
248     * Clear all device info.
249     *
250     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
251     */
252    void clearDeviceInfoList() {
253        assertRunOnServiceThread();
254        mDeviceInfos.clear();
255    }
256
257    /**
258     * Return a list of all {@link HdmiCecDeviceInfo}.
259     *
260     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
261     */
262    List<HdmiCecDeviceInfo> getDeviceInfoList() {
263        assertRunOnServiceThread();
264        return sparseArrayToList(mDeviceInfos);
265    }
266
267    /**
268     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
269     *
270     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
271     *
272     * @param logicalAddress logical address to be retrieved
273     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
274     *         Returns null if no logical address matched
275     */
276    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
277        assertRunOnServiceThread();
278        return mDeviceInfos.get(logicalAddress);
279    }
280
281    /**
282     * Return the locally hosted logical device of a given type.
283     *
284     * @param deviceType logical device type
285     * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available;
286     *          otherwise null.
287     */
288    HdmiCecLocalDevice getLocalDevice(int deviceType) {
289        return mLocalDevices.get(deviceType);
290    }
291
292    /**
293     * Add a new logical address to the device. Device's HW should be notified
294     * when a new logical address is assigned to a device, so that it can accept
295     * a command having available destinations.
296     *
297     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
298     *
299     * @param newLogicalAddress a logical address to be added
300     * @return 0 on success. Otherwise, returns negative value
301     */
302    int addLogicalAddress(int newLogicalAddress) {
303        assertRunOnServiceThread();
304        if (HdmiCec.isValidAddress(newLogicalAddress)) {
305            return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
306        } else {
307            return -1;
308        }
309    }
310
311    /**
312     * Clear all logical addresses registered in the device.
313     *
314     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
315     */
316    void clearLogicalAddress() {
317        assertRunOnServiceThread();
318        // TODO: consider to backup logical address so that new logical address
319        // allocation can use it as preferred address.
320        for (int i = 0; i < mLocalDevices.size(); ++i) {
321            mLocalDevices.valueAt(i).clearAddress();
322        }
323        nativeClearLogicalAddress(mNativePtr);
324    }
325
326    /**
327     * Return the physical address of the device.
328     *
329     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
330     *
331     * @return CEC physical address of the device. The range of success address
332     *         is between 0x0000 and 0xFFFF. If failed it returns -1
333     */
334    int getPhysicalAddress() {
335        assertRunOnServiceThread();
336        return nativeGetPhysicalAddress(mNativePtr);
337    }
338
339    /**
340     * Return CEC version of the device.
341     *
342     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
343     */
344    int getVersion() {
345        assertRunOnServiceThread();
346        return nativeGetVersion(mNativePtr);
347    }
348
349    /**
350     * Return vendor id of the device.
351     *
352     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
353     */
354    int getVendorId() {
355        assertRunOnServiceThread();
356        return nativeGetVendorId(mNativePtr);
357    }
358
359    /**
360     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
361     * devices.
362     *
363     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
364     *
365     * @param callback an interface used to get a list of all remote devices' address
366     * @param retryCount the number of retry used to send polling message to remote devices
367     */
368    void pollDevices(DevicePollingCallback callback, int retryCount) {
369        assertRunOnServiceThread();
370        // Extract polling candidates. No need to poll against local devices.
371        ArrayList<Integer> pollingCandidates = new ArrayList<>();
372        for (int i = HdmiCec.ADDR_SPECIFIC_USE; i >= HdmiCec.ADDR_TV; --i) {
373            if (!isAllocatedLocalDeviceAddress(i)) {
374                pollingCandidates.add(i);
375            }
376        }
377
378        runDevicePolling(pollingCandidates, retryCount, callback);
379    }
380
381    /**
382     * Return a list of all {@link HdmiCecLocalDevice}s.
383     *
384     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
385     */
386    List<HdmiCecLocalDevice> getLocalDeviceList() {
387        assertRunOnServiceThread();
388        return sparseArrayToList(mLocalDevices);
389    }
390
391    private static <T> List<T> sparseArrayToList(SparseArray<T> array) {
392        ArrayList<T> list = new ArrayList<>();
393        for (int i = 0; i < array.size(); ++i) {
394            list.add(array.valueAt(i));
395        }
396        return list;
397    }
398
399    private boolean isAllocatedLocalDeviceAddress(int address) {
400        for (int i = 0; i < mLocalDevices.size(); ++i) {
401            if (mLocalDevices.valueAt(i).isAddressOf(address)) {
402                return true;
403            }
404        }
405        return false;
406    }
407
408    private void runDevicePolling(final List<Integer> candidates, final int retryCount,
409            final DevicePollingCallback callback) {
410        assertRunOnServiceThread();
411        runOnIoThread(new Runnable() {
412            @Override
413            public void run() {
414                final ArrayList<Integer> allocated = new ArrayList<>();
415                for (Integer address : candidates) {
416                    if (sendPollMessage(address, retryCount)) {
417                        allocated.add(address);
418                    }
419                }
420                if (callback != null) {
421                    runOnServiceThread(new Runnable() {
422                        @Override
423                        public void run() {
424                            callback.onPollingFinished(allocated);
425                        }
426                    });
427                }
428            }
429        });
430    }
431
432    private boolean sendPollMessage(int address, int retryCount) {
433        assertRunOnIoThread();
434        for (int i = 0; i < retryCount; ++i) {
435            // <Polling Message> is a message which has empty body and
436            // uses same address for both source and destination address.
437            // If sending <Polling Message> failed (NAK), it becomes
438            // new logical address for the device because no device uses
439            // it as logical address of the device.
440            if (nativeSendCecCommand(mNativePtr, address, address, EMPTY_BODY)
441                    == HdmiControlService.SEND_RESULT_SUCCESS) {
442                return true;
443            }
444        }
445        return false;
446    }
447
448    private void assertRunOnIoThread() {
449        if (Looper.myLooper() != mIoHandler.getLooper()) {
450            throw new IllegalStateException("Should run on io thread.");
451        }
452    }
453
454    private void assertRunOnServiceThread() {
455        if (Looper.myLooper() != mControlHandler.getLooper()) {
456            throw new IllegalStateException("Should run on service thread.");
457        }
458    }
459
460    // Run a Runnable on IO thread.
461    // It should be careful to access member variables on IO thread because
462    // it can be accessed from system thread as well.
463    private void runOnIoThread(Runnable runnable) {
464        mIoHandler.post(runnable);
465    }
466
467    private void runOnServiceThread(Runnable runnable) {
468        mControlHandler.post(runnable);
469    }
470
471    private boolean isAcceptableAddress(int address) {
472        // Can access command targeting devices available in local device or broadcast command.
473        if (address == HdmiCec.ADDR_BROADCAST) {
474            return true;
475        }
476        return isAllocatedLocalDeviceAddress(address);
477    }
478
479    private void onReceiveCommand(HdmiCecMessage message) {
480        assertRunOnServiceThread();
481        if (isAcceptableAddress(message.getDestination())
482                && mService.handleCecCommand(message)) {
483            return;
484        }
485
486        if (message.getDestination() != HdmiCec.ADDR_BROADCAST) {
487            int sourceAddress = message.getDestination();
488            // Reply <Feature Abort> to initiator (source) for all requests.
489            HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
490                    sourceAddress, message.getSource(), message.getOpcode(),
491                    HdmiConstants.ABORT_REFUSED);
492            sendCommand(cecMessage, null);
493        }
494    }
495
496    void sendCommand(HdmiCecMessage cecMessage) {
497        sendCommand(cecMessage, null);
498    }
499
500    void sendCommand(final HdmiCecMessage cecMessage,
501            final HdmiControlService.SendMessageCallback callback) {
502        runOnIoThread(new Runnable() {
503            @Override
504            public void run() {
505                byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
506                final int error = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
507                        cecMessage.getDestination(), body);
508                if (error != HdmiControlService.SEND_RESULT_SUCCESS) {
509                    Slog.w(TAG, "Failed to send " + cecMessage);
510                }
511                if (callback != null) {
512                    runOnServiceThread(new Runnable() {
513                        @Override
514                        public void run() {
515                            callback.onSendCompleted(error);
516                        }
517                    });
518                }
519            }
520        });
521    }
522
523    /**
524     * Called by native when incoming CEC message arrived.
525     */
526    private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
527        assertRunOnServiceThread();
528        onReceiveCommand(HdmiCecMessageBuilder.of(srcAddress, dstAddress, body));
529    }
530
531    /**
532     * Called by native when a hotplug event issues.
533     */
534    private void handleHotplug(boolean connected) {
535        // TODO: once add port number to cec HAL interface, pass port number
536        // to the service.
537        mService.onHotplug(0, connected);
538    }
539
540    private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
541    private static native int nativeSendCecCommand(long controllerPtr, int srcAddress,
542            int dstAddress, byte[] body);
543    private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress);
544    private static native void nativeClearLogicalAddress(long controllerPtr);
545    private static native int nativeGetPhysicalAddress(long controllerPtr);
546    private static native int nativeGetVersion(long controllerPtr);
547    private static native int nativeGetVendorId(long controllerPtr);
548}
549