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