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