HdmiCecController.java revision a5b7414970c85217e88015e78ecbc5ba093dead3
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, RETRY_COUNT_FOR_LOGICAL_ADDRESS_ALLOCATION)) {
204                    logicalAddress = curAddress;
205                    break;
206                }
207            }
208        }
209
210        final int assignedAddress = logicalAddress;
211        if (callback != null) {
212            runOnServiceThread(new Runnable() {
213                    @Override
214                public void run() {
215                    callback.onAllocated(deviceType, assignedAddress);
216                }
217            });
218        }
219    }
220
221    private static byte[] buildBody(int opcode, byte[] params) {
222        byte[] body = new byte[params.length + 1];
223        body[0] = (byte) opcode;
224        System.arraycopy(params, 0, body, 1, params.length);
225        return body;
226    }
227
228
229    HdmiPortInfo[] getPortInfos() {
230        return nativeGetPortInfos(mNativePtr);
231    }
232
233    /**
234     * Return the locally hosted logical device of a given type.
235     *
236     * @param deviceType logical device type
237     * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available;
238     *          otherwise null.
239     */
240    HdmiCecLocalDevice getLocalDevice(int deviceType) {
241        return mLocalDevices.get(deviceType);
242    }
243
244    /**
245     * Add a new logical address to the device. Device's HW should be notified
246     * when a new logical address is assigned to a device, so that it can accept
247     * a command having available destinations.
248     *
249     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
250     *
251     * @param newLogicalAddress a logical address to be added
252     * @return 0 on success. Otherwise, returns negative value
253     */
254    @ServiceThreadOnly
255    int addLogicalAddress(int newLogicalAddress) {
256        assertRunOnServiceThread();
257        if (HdmiCec.isValidAddress(newLogicalAddress)) {
258            return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
259        } else {
260            return -1;
261        }
262    }
263
264    /**
265     * Clear all logical addresses registered in the device.
266     *
267     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
268     */
269    @ServiceThreadOnly
270    void clearLogicalAddress() {
271        assertRunOnServiceThread();
272        // TODO: consider to backup logical address so that new logical address
273        // allocation can use it as preferred address.
274        for (int i = 0; i < mLocalDevices.size(); ++i) {
275            mLocalDevices.valueAt(i).clearAddress();
276        }
277        nativeClearLogicalAddress(mNativePtr);
278    }
279
280    /**
281     * Return the physical address of the device.
282     *
283     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
284     *
285     * @return CEC physical address of the device. The range of success address
286     *         is between 0x0000 and 0xFFFF. If failed it returns -1
287     */
288    @ServiceThreadOnly
289    int getPhysicalAddress() {
290        assertRunOnServiceThread();
291        return nativeGetPhysicalAddress(mNativePtr);
292    }
293
294    /**
295     * Return CEC version of the device.
296     *
297     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
298     */
299    @ServiceThreadOnly
300    int getVersion() {
301        assertRunOnServiceThread();
302        return nativeGetVersion(mNativePtr);
303    }
304
305    /**
306     * Return vendor id of the device.
307     *
308     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
309     */
310    @ServiceThreadOnly
311    int getVendorId() {
312        assertRunOnServiceThread();
313        return nativeGetVendorId(mNativePtr);
314    }
315
316    /**
317     * Pass a option to CEC HAL.
318     *
319     * @param flag a key of option. For more details, look at
320     *        {@link HdmiConstants#FLAG_HDMI_OPTION_WAKEUP} to
321     *        {@link HdmiConstants#FLAG_HDMI_OPTION_SYSTEM_CEC_CONTROL}
322     * @param value a value of option. Actual value varies flag. For more
323     *        details, look at description of flags
324     */
325    @ServiceThreadOnly
326    void setOption(int flag, int value) {
327        assertRunOnServiceThread();
328        nativeSetOption(mNativePtr, flag, value);
329    }
330
331    /**
332     * Configure ARC circuit in the hardware logic to start or stop the feature.
333     *
334     * @param enabled whether to enable/disable ARC
335     */
336    @ServiceThreadOnly
337    void setAudioReturnChannel(boolean enabled) {
338        assertRunOnServiceThread();
339        nativeSetAudioReturnChannel(mNativePtr, enabled);
340    }
341
342    /**
343     * Return the connection status of the specified port
344     *
345     * @param port port number to check connection status
346     * @return true if connected; otherwise, return false
347     */
348    @ServiceThreadOnly
349    boolean isConnected(int port) {
350        assertRunOnServiceThread();
351        return nativeIsConnected(mNativePtr, port);
352    }
353
354    /**
355     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
356     * devices.
357     *
358     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
359     *
360     * @param callback an interface used to get a list of all remote devices' address
361     * @param pickStrategy strategy how to pick polling candidates
362     * @param retryCount the number of retry used to send polling message to remote devices
363     */
364    @ServiceThreadOnly
365    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
366        assertRunOnServiceThread();
367
368        // Extract polling candidates. No need to poll against local devices.
369        List<Integer> pollingCandidates = pickPollCandidates(pickStrategy);
370        runDevicePolling(pollingCandidates, retryCount, callback);
371    }
372
373    /**
374     * Return a list of all {@link HdmiCecLocalDevice}s.
375     *
376     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
377     */
378    @ServiceThreadOnly
379    List<HdmiCecLocalDevice> getLocalDeviceList() {
380        assertRunOnServiceThread();
381        return HdmiUtils.sparseArrayToList(mLocalDevices);
382    }
383
384    private List<Integer> pickPollCandidates(int pickStrategy) {
385        int strategy = pickStrategy & HdmiConstants.POLL_STRATEGY_MASK;
386        Predicate<Integer> pickPredicate = null;
387        switch (strategy) {
388            case HdmiConstants.POLL_STRATEGY_SYSTEM_AUDIO:
389                pickPredicate = mSystemAudioAddressPredicate;
390                break;
391            case HdmiConstants.POLL_STRATEGY_REMOTES_DEVICES:
392            default:  // The default is POLL_STRATEGY_REMOTES_DEVICES.
393                pickPredicate = mRemoteDeviceAddressPredicate;
394                break;
395        }
396
397        int iterationStrategy = pickStrategy & HdmiConstants.POLL_ITERATION_STRATEGY_MASK;
398        ArrayList<Integer> pollingCandidates = new ArrayList<>();
399        switch (iterationStrategy) {
400            case HdmiConstants.POLL_ITERATION_IN_ORDER:
401                for (int i = HdmiCec.ADDR_TV; i <= HdmiCec.ADDR_SPECIFIC_USE; ++i) {
402                    if (pickPredicate.apply(i)) {
403                        pollingCandidates.add(i);
404                    }
405                }
406                break;
407            case HdmiConstants.POLL_ITERATION_REVERSE_ORDER:
408            default:  // The default is reverse order.
409                for (int i = HdmiCec.ADDR_SPECIFIC_USE; i >= HdmiCec.ADDR_TV; --i) {
410                    if (pickPredicate.apply(i)) {
411                        pollingCandidates.add(i);
412                    }
413                }
414                break;
415        }
416        return pollingCandidates;
417    }
418
419    @ServiceThreadOnly
420    private boolean isAllocatedLocalDeviceAddress(int address) {
421        assertRunOnServiceThread();
422        for (int i = 0; i < mLocalDevices.size(); ++i) {
423            if (mLocalDevices.valueAt(i).isAddressOf(address)) {
424                return true;
425            }
426        }
427        return false;
428    }
429
430    @ServiceThreadOnly
431    private void runDevicePolling(final List<Integer> candidates, final int retryCount,
432            final DevicePollingCallback callback) {
433        assertRunOnServiceThread();
434        runOnIoThread(new Runnable() {
435            @Override
436            public void run() {
437                final ArrayList<Integer> allocated = new ArrayList<>();
438                for (Integer address : candidates) {
439                    if (sendPollMessage(address, retryCount)) {
440                        allocated.add(address);
441                    }
442                }
443                if (callback != null) {
444                    runOnServiceThread(new Runnable() {
445                        @Override
446                        public void run() {
447                            callback.onPollingFinished(allocated);
448                        }
449                    });
450                }
451            }
452        });
453    }
454
455    @IoThreadOnly
456    private boolean sendPollMessage(int address, int retryCount) {
457        assertRunOnIoThread();
458        for (int i = 0; i < retryCount; ++i) {
459            // <Polling Message> is a message which has empty body and
460            // uses same address for both source and destination address.
461            // If sending <Polling Message> failed (NAK), it becomes
462            // new logical address for the device because no device uses
463            // it as logical address of the device.
464            if (nativeSendCecCommand(mNativePtr, address, address, EMPTY_BODY)
465                    == HdmiConstants.SEND_RESULT_SUCCESS) {
466                return true;
467            }
468        }
469        return false;
470    }
471
472    private void assertRunOnIoThread() {
473        if (Looper.myLooper() != mIoHandler.getLooper()) {
474            throw new IllegalStateException("Should run on io thread.");
475        }
476    }
477
478    private void assertRunOnServiceThread() {
479        if (Looper.myLooper() != mControlHandler.getLooper()) {
480            throw new IllegalStateException("Should run on service thread.");
481        }
482    }
483
484    // Run a Runnable on IO thread.
485    // It should be careful to access member variables on IO thread because
486    // it can be accessed from system thread as well.
487    private void runOnIoThread(Runnable runnable) {
488        mIoHandler.post(runnable);
489    }
490
491    private void runOnServiceThread(Runnable runnable) {
492        mControlHandler.post(runnable);
493    }
494
495    private boolean isAcceptableAddress(int address) {
496        // Can access command targeting devices available in local device or broadcast command.
497        if (address == HdmiCec.ADDR_BROADCAST) {
498            return true;
499        }
500        return isAllocatedLocalDeviceAddress(address);
501    }
502
503    @ServiceThreadOnly
504    private void onReceiveCommand(HdmiCecMessage message) {
505        assertRunOnServiceThread();
506        if (isAcceptableAddress(message.getDestination())
507                && mService.handleCecCommand(message)) {
508            return;
509        }
510
511        if (message.getDestination() != HdmiCec.ADDR_BROADCAST) {
512            int sourceAddress = message.getDestination();
513            // Reply <Feature Abort> to initiator (source) for all requests.
514            HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
515                    sourceAddress, message.getSource(), message.getOpcode(),
516                    HdmiConstants.ABORT_REFUSED);
517            sendCommand(cecMessage);
518        }
519    }
520
521    @ServiceThreadOnly
522    void sendCommand(HdmiCecMessage cecMessage) {
523        assertRunOnServiceThread();
524        sendCommand(cecMessage, null);
525    }
526
527    @ServiceThreadOnly
528    void sendCommand(final HdmiCecMessage cecMessage,
529            final HdmiControlService.SendMessageCallback callback) {
530        assertRunOnServiceThread();
531        runOnIoThread(new Runnable() {
532            @Override
533            public void run() {
534                byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
535                final int error = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
536                        cecMessage.getDestination(), body);
537                if (error != HdmiConstants.SEND_RESULT_SUCCESS) {
538                    Slog.w(TAG, "Failed to send " + cecMessage);
539                }
540                if (callback != null) {
541                    runOnServiceThread(new Runnable() {
542                        @Override
543                        public void run() {
544                            callback.onSendCompleted(error);
545                        }
546                    });
547                }
548            }
549        });
550    }
551
552    /**
553     * Called by native when incoming CEC message arrived.
554     */
555    @ServiceThreadOnly
556    private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
557        assertRunOnServiceThread();
558        onReceiveCommand(HdmiCecMessageBuilder.of(srcAddress, dstAddress, body));
559    }
560
561    /**
562     * Called by native when a hotplug event issues.
563     */
564    private void handleHotplug(boolean connected) {
565        // TODO: once add port number to cec HAL interface, pass port number
566        // to the service.
567        mService.onHotplug(0, connected);
568    }
569
570    private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
571    private static native int nativeSendCecCommand(long controllerPtr, int srcAddress,
572            int dstAddress, byte[] body);
573    private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress);
574    private static native void nativeClearLogicalAddress(long controllerPtr);
575    private static native int nativeGetPhysicalAddress(long controllerPtr);
576    private static native int nativeGetVersion(long controllerPtr);
577    private static native int nativeGetVendorId(long controllerPtr);
578    private static native HdmiPortInfo[] nativeGetPortInfos(long controllerPtr);
579    private static native void nativeSetOption(long controllerPtr, int flag, int value);
580    private static native void nativeSetAudioReturnChannel(long controllerPtr, boolean flag);
581    private static native boolean nativeIsConnected(long controllerPtr, int port);
582}
583