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