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