HdmiCecController.java revision 092b445ef898e3c1e5b2918b554480940f0f5a28
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.internal.util.Predicate;
29import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
30
31import libcore.util.EmptyArray;
32
33import java.util.ArrayList;
34import java.util.List;
35
36/**
37 * Manages HDMI-CEC command and behaviors. It converts user's command into CEC command
38 * and pass it to CEC HAL so that it sends message to other device. For incoming
39 * message it translates the message and delegates it to proper module.
40 *
41 * <p>It should be careful to access member variables on IO thread because
42 * it can be accessed from system thread as well.
43 *
44 * <p>It can be created only by {@link HdmiCecController#create}
45 *
46 * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
47 */
48final class HdmiCecController {
49    private static final String TAG = "HdmiCecController";
50
51    /**
52     * Interface to report allocated logical address.
53     */
54    interface AllocateAddressCallback {
55        /**
56         * Called when a new logical address is allocated.
57         *
58         * @param deviceType requested device type to allocate logical address
59         * @param logicalAddress allocated logical address. If it is
60         *                       {@link HdmiCec#ADDR_UNREGISTERED}, it means that
61         *                       it failed to allocate logical address for the given device type
62         */
63        void onAllocated(int deviceType, int logicalAddress);
64    }
65
66    private static final byte[] EMPTY_BODY = EmptyArray.BYTE;
67
68    // A message to pass cec send command to IO looper.
69    private static final int MSG_SEND_CEC_COMMAND = 1;
70    // A message to delegate logical allocation to IO looper.
71    private static final int MSG_ALLOCATE_LOGICAL_ADDRESS = 2;
72
73    // Message types to handle incoming message in main service looper.
74    private final static int MSG_RECEIVE_CEC_COMMAND = 1;
75    // A message to report allocated logical address to main control looper.
76    private final static int MSG_REPORT_LOGICAL_ADDRESS = 2;
77
78    private static final int NUM_LOGICAL_ADDRESS = 16;
79
80    private static final int RETRY_COUNT_FOR_LOGICAL_ADDRESS_ALLOCATION = 3;
81
82    // Predicate for whether the given logical address is remote device's one or not.
83    private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() {
84        @Override
85        public boolean apply(Integer address) {
86            return !isAllocatedLocalDeviceAddress(address);
87        }
88    };
89
90    // Predicate whether the given logical address is system audio's one or not
91    private final Predicate<Integer> mSystemAudioAddressPredicate = new Predicate<Integer>() {
92        @Override
93        public boolean apply(Integer address) {
94            return HdmiCec.getTypeFromAddress(address) == HdmiCec.ADDR_AUDIO_SYSTEM;
95        }
96    };
97
98    // Handler instance to process synchronous I/O (mainly send) message.
99    private Handler mIoHandler;
100
101    // Handler instance to process various messages coming from other CEC
102    // device or issued by internal state change.
103    private Handler mControlHandler;
104
105    // Stores the pointer to the native implementation of the service that
106    // interacts with HAL.
107    private volatile long mNativePtr;
108
109    private HdmiControlService mService;
110
111    // Map-like container of all cec devices including local ones.
112    // A logical address of device is used as key of container.
113    private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>();
114
115    // Stores the local CEC devices in the system. Device type is used for key.
116    private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
117
118    // Private constructor.  Use HdmiCecController.create().
119    private HdmiCecController() {
120    }
121
122    /**
123     * A factory method to get {@link HdmiCecController}. If it fails to initialize
124     * inner device or has no device it will return {@code null}.
125     *
126     * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
127     * @param service {@link HdmiControlService} instance used to create internal handler
128     *                and to pass callback for incoming message or event.
129     * @return {@link HdmiCecController} if device is initialized successfully. Otherwise,
130     *         returns {@code null}.
131     */
132    static HdmiCecController create(HdmiControlService service) {
133        HdmiCecController controller = new HdmiCecController();
134        long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
135        if (nativePtr == 0L) {
136            controller = null;
137            return null;
138        }
139
140        controller.init(service, nativePtr);
141        return controller;
142    }
143
144    private void init(HdmiControlService service, long nativePtr) {
145        mService = service;
146        mIoHandler = new Handler(service.getServiceLooper());
147        mControlHandler = new Handler(service.getServiceLooper());
148        mNativePtr = nativePtr;
149    }
150
151    void addLocalDevice(int deviceType, HdmiCecLocalDevice device) {
152        mLocalDevices.put(deviceType, device);
153    }
154
155    /**
156     * Allocate a new logical address of the given device type. Allocated
157     * address will be reported through {@link AllocateAddressCallback}.
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 AllocateAddressCallback 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 AllocateAddressCallback 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     * @param includeLocalDevice whether to add local device or not
280     */
281    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
282        assertRunOnServiceThread();
283        if (includeLocalDevice) {
284            return sparseArrayToList(mDeviceInfos);
285        } else {
286            ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>();
287            for (int i = 0; i < mDeviceInfos.size(); ++i) {
288                HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i);
289                if (mRemoteDeviceAddressPredicate.apply(info.getLogicalAddress())) {
290                    infoList.add(info);
291                }
292            }
293            return infoList;
294        }
295    }
296
297    /**
298     * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}.
299     *
300     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
301     *
302     * @param logicalAddress logical address to be retrieved
303     * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}.
304     *         Returns null if no logical address matched
305     */
306    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
307        assertRunOnServiceThread();
308        return mDeviceInfos.get(logicalAddress);
309    }
310
311    /**
312     * Return the locally hosted logical device of a given type.
313     *
314     * @param deviceType logical device type
315     * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available;
316     *          otherwise null.
317     */
318    HdmiCecLocalDevice getLocalDevice(int deviceType) {
319        return mLocalDevices.get(deviceType);
320    }
321
322    /**
323     * Add a new logical address to the device. Device's HW should be notified
324     * when a new logical address is assigned to a device, so that it can accept
325     * a command having available destinations.
326     *
327     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
328     *
329     * @param newLogicalAddress a logical address to be added
330     * @return 0 on success. Otherwise, returns negative value
331     */
332    int addLogicalAddress(int newLogicalAddress) {
333        assertRunOnServiceThread();
334        if (HdmiCec.isValidAddress(newLogicalAddress)) {
335            return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
336        } else {
337            return -1;
338        }
339    }
340
341    /**
342     * Clear all logical addresses registered in the device.
343     *
344     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
345     */
346    void clearLogicalAddress() {
347        assertRunOnServiceThread();
348        // TODO: consider to backup logical address so that new logical address
349        // allocation can use it as preferred address.
350        for (int i = 0; i < mLocalDevices.size(); ++i) {
351            mLocalDevices.valueAt(i).clearAddress();
352        }
353        nativeClearLogicalAddress(mNativePtr);
354    }
355
356    /**
357     * Return the physical address of the device.
358     *
359     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
360     *
361     * @return CEC physical address of the device. The range of success address
362     *         is between 0x0000 and 0xFFFF. If failed it returns -1
363     */
364    int getPhysicalAddress() {
365        assertRunOnServiceThread();
366        return nativeGetPhysicalAddress(mNativePtr);
367    }
368
369    /**
370     * Return CEC version of the device.
371     *
372     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
373     */
374    int getVersion() {
375        assertRunOnServiceThread();
376        return nativeGetVersion(mNativePtr);
377    }
378
379    /**
380     * Return vendor id of the device.
381     *
382     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
383     */
384    int getVendorId() {
385        assertRunOnServiceThread();
386        return nativeGetVendorId(mNativePtr);
387    }
388
389    /**
390     * Pass a option to CEC HAL.
391     *
392     * @param flag a key of option. For more details, look at
393     *        {@link HdmiConstants#FLAG_HDMI_OPTION_WAKEUP} to
394     *        {@link HdmiConstants#FLAG_HDMI_OPTION_SYSTEM_CEC_CONTROL}
395     * @param value a value of option. Actual value varies flag. For more
396     *        details, look at description of flags
397     */
398    void setOption(int flag, int value) {
399        assertRunOnServiceThread();
400        nativeSetOption(mNativePtr, flag, value);
401    }
402
403    /**
404     * Configure ARC circuit in the hardware logic to start or stop the feature.
405     *
406     * @param enabled whether to enable/disable ARC
407     */
408    void setAudioReturnChannel(boolean enabled) {
409        assertRunOnServiceThread();
410        nativeSetAudioReturnChannel(mNativePtr, enabled);
411    }
412
413    /**
414     * Return the connection status of the specified port
415     *
416     * @param port port number to check connection status
417     * @return true if connected; otherwise, return false
418     */
419    boolean isConnected(int port) {
420        assertRunOnServiceThread();
421        return nativeIsConnected(mNativePtr, port);
422    }
423
424    /**
425     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
426     * devices.
427     *
428     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
429     *
430     * @param callback an interface used to get a list of all remote devices' address
431     * @param pickStrategy strategy how to pick polling candidates
432     * @param retryCount the number of retry used to send polling message to remote devices
433     */
434    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
435        assertRunOnServiceThread();
436
437        // Extract polling candidates. No need to poll against local devices.
438        List<Integer> pollingCandidates = pickPollCandidates(pickStrategy);
439        runDevicePolling(pollingCandidates, retryCount, callback);
440    }
441
442    /**
443     * Return a list of all {@link HdmiCecLocalDevice}s.
444     *
445     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
446     */
447    List<HdmiCecLocalDevice> getLocalDeviceList() {
448        assertRunOnServiceThread();
449        return sparseArrayToList(mLocalDevices);
450    }
451
452    private List<Integer> pickPollCandidates(int pickStrategy) {
453        int strategy = pickStrategy & HdmiControlService.POLL_STRATEGY_MASK;
454        Predicate<Integer> pickPredicate = null;
455        switch (strategy) {
456            case HdmiControlService.POLL_STRATEGY_SYSTEM_AUDIO:
457                pickPredicate = mSystemAudioAddressPredicate;
458                break;
459            case HdmiControlService.POLL_STRATEGY_REMOTES_DEVICES:
460            default:  // The default is POLL_STRATEGY_REMOTES_DEVICES.
461                pickPredicate = mRemoteDeviceAddressPredicate;
462                break;
463        }
464
465        int iterationStrategy = pickStrategy & HdmiControlService.POLL_ITERATION_STRATEGY_MASK;
466        ArrayList<Integer> pollingCandidates = new ArrayList<>();
467        switch (iterationStrategy) {
468            case HdmiControlService.POLL_ITERATION_IN_ORDER:
469                for (int i = HdmiCec.ADDR_TV; i <= HdmiCec.ADDR_SPECIFIC_USE; ++i) {
470                    if (pickPredicate.apply(i)) {
471                        pollingCandidates.add(i);
472                    }
473                }
474                break;
475            case HdmiControlService.POLL_ITERATION_REVERSE_ORDER:
476            default:  // The default is reverse order.
477                for (int i = HdmiCec.ADDR_SPECIFIC_USE; i >= HdmiCec.ADDR_TV; --i) {
478                    if (pickPredicate.apply(i)) {
479                        pollingCandidates.add(i);
480                    }
481                }
482                break;
483        }
484        return pollingCandidates;
485    }
486
487    private static <T> List<T> sparseArrayToList(SparseArray<T> array) {
488        ArrayList<T> list = new ArrayList<>();
489        for (int i = 0; i < array.size(); ++i) {
490            list.add(array.valueAt(i));
491        }
492        return list;
493    }
494
495    private boolean isAllocatedLocalDeviceAddress(int address) {
496        for (int i = 0; i < mLocalDevices.size(); ++i) {
497            if (mLocalDevices.valueAt(i).isAddressOf(address)) {
498                return true;
499            }
500        }
501        return false;
502    }
503
504    private void runDevicePolling(final List<Integer> candidates, final int retryCount,
505            final DevicePollingCallback callback) {
506        assertRunOnServiceThread();
507        runOnIoThread(new Runnable() {
508            @Override
509            public void run() {
510                final ArrayList<Integer> allocated = new ArrayList<>();
511                for (Integer address : candidates) {
512                    if (sendPollMessage(address, retryCount)) {
513                        allocated.add(address);
514                    }
515                }
516                if (callback != null) {
517                    runOnServiceThread(new Runnable() {
518                        @Override
519                        public void run() {
520                            callback.onPollingFinished(allocated);
521                        }
522                    });
523                }
524            }
525        });
526    }
527
528    private boolean sendPollMessage(int address, int retryCount) {
529        assertRunOnIoThread();
530        for (int i = 0; i < retryCount; ++i) {
531            // <Polling Message> is a message which has empty body and
532            // uses same address for both source and destination address.
533            // If sending <Polling Message> failed (NAK), it becomes
534            // new logical address for the device because no device uses
535            // it as logical address of the device.
536            if (nativeSendCecCommand(mNativePtr, address, address, EMPTY_BODY)
537                    == HdmiControlService.SEND_RESULT_SUCCESS) {
538                return true;
539            }
540        }
541        return false;
542    }
543
544    private void assertRunOnIoThread() {
545        if (Looper.myLooper() != mIoHandler.getLooper()) {
546            throw new IllegalStateException("Should run on io thread.");
547        }
548    }
549
550    private void assertRunOnServiceThread() {
551        if (Looper.myLooper() != mControlHandler.getLooper()) {
552            throw new IllegalStateException("Should run on service thread.");
553        }
554    }
555
556    // Run a Runnable on IO thread.
557    // It should be careful to access member variables on IO thread because
558    // it can be accessed from system thread as well.
559    private void runOnIoThread(Runnable runnable) {
560        mIoHandler.post(runnable);
561    }
562
563    private void runOnServiceThread(Runnable runnable) {
564        mControlHandler.post(runnable);
565    }
566
567    private boolean isAcceptableAddress(int address) {
568        // Can access command targeting devices available in local device or broadcast command.
569        if (address == HdmiCec.ADDR_BROADCAST) {
570            return true;
571        }
572        return isAllocatedLocalDeviceAddress(address);
573    }
574
575    private void onReceiveCommand(HdmiCecMessage message) {
576        assertRunOnServiceThread();
577        if (isAcceptableAddress(message.getDestination())
578                && mService.handleCecCommand(message)) {
579            return;
580        }
581
582        if (message.getDestination() != HdmiCec.ADDR_BROADCAST) {
583            int sourceAddress = message.getDestination();
584            // Reply <Feature Abort> to initiator (source) for all requests.
585            HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
586                    sourceAddress, message.getSource(), message.getOpcode(),
587                    HdmiConstants.ABORT_REFUSED);
588            sendCommand(cecMessage, null);
589        }
590    }
591
592    void sendCommand(HdmiCecMessage cecMessage) {
593        sendCommand(cecMessage, null);
594    }
595
596    void sendCommand(final HdmiCecMessage cecMessage,
597            final HdmiControlService.SendMessageCallback callback) {
598        runOnIoThread(new Runnable() {
599            @Override
600            public void run() {
601                byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
602                final int error = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
603                        cecMessage.getDestination(), body);
604                if (error != HdmiControlService.SEND_RESULT_SUCCESS) {
605                    Slog.w(TAG, "Failed to send " + cecMessage);
606                }
607                if (callback != null) {
608                    runOnServiceThread(new Runnable() {
609                        @Override
610                        public void run() {
611                            callback.onSendCompleted(error);
612                        }
613                    });
614                }
615            }
616        });
617    }
618
619    /**
620     * Called by native when incoming CEC message arrived.
621     */
622    private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
623        assertRunOnServiceThread();
624        onReceiveCommand(HdmiCecMessageBuilder.of(srcAddress, dstAddress, body));
625    }
626
627    /**
628     * Called by native when a hotplug event issues.
629     */
630    private void handleHotplug(boolean connected) {
631        // TODO: once add port number to cec HAL interface, pass port number
632        // to the service.
633        mService.onHotplug(0, connected);
634    }
635
636    private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
637    private static native int nativeSendCecCommand(long controllerPtr, int srcAddress,
638            int dstAddress, byte[] body);
639    private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress);
640    private static native void nativeClearLogicalAddress(long controllerPtr);
641    private static native int nativeGetPhysicalAddress(long controllerPtr);
642    private static native int nativeGetVersion(long controllerPtr);
643    private static native int nativeGetVendorId(long controllerPtr);
644    private static native void nativeSetOption(long controllerPtr, int flag, int value);
645    private static native void nativeSetAudioReturnChannel(long controllerPtr, boolean flag);
646    private static native boolean nativeIsConnected(long controllerPtr, int port);
647
648}
649