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