HdmiCecController.java revision 7df52862dae1fa33c84725c613b0d9b88c1b28b6
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.HdmiPortInfo;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.MessageQueue;
23import android.util.Slog;
24import android.util.SparseArray;
25
26import com.android.internal.util.Predicate;
27import com.android.server.hdmi.HdmiAnnotations.IoThreadOnly;
28import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
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 Constants#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    private static final int NUM_LOGICAL_ADDRESS = 16;
69
70    // Predicate for whether the given logical address is remote device's one or not.
71    private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() {
72        @Override
73        public boolean apply(Integer address) {
74            return !isAllocatedLocalDeviceAddress(address);
75        }
76    };
77
78    // Predicate whether the given logical address is system audio's one or not
79    private final Predicate<Integer> mSystemAudioAddressPredicate = new Predicate<Integer>() {
80        @Override
81        public boolean apply(Integer address) {
82            return HdmiUtils.getTypeFromAddress(address) == Constants.ADDR_AUDIO_SYSTEM;
83        }
84    };
85
86    // Handler instance to process synchronous I/O (mainly send) message.
87    private Handler mIoHandler;
88
89    // Handler instance to process various messages coming from other CEC
90    // device or issued by internal state change.
91    private Handler mControlHandler;
92
93    // Stores the pointer to the native implementation of the service that
94    // interacts with HAL.
95    private volatile long mNativePtr;
96
97    private final HdmiControlService mService;
98
99    // Stores the local CEC devices in the system. Device type is used for key.
100    private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
101
102    // Private constructor.  Use HdmiCecController.create().
103    private HdmiCecController(HdmiControlService service) {
104        mService = service;
105    }
106
107    /**
108     * A factory method to get {@link HdmiCecController}. If it fails to initialize
109     * inner device or has no device it will return {@code null}.
110     *
111     * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
112     * @param service {@link HdmiControlService} instance used to create internal handler
113     *                and to pass callback for incoming message or event.
114     * @return {@link HdmiCecController} if device is initialized successfully. Otherwise,
115     *         returns {@code null}.
116     */
117    static HdmiCecController create(HdmiControlService service) {
118        HdmiCecController controller = new HdmiCecController(service);
119        long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
120        if (nativePtr == 0L) {
121            controller = null;
122            return null;
123        }
124
125        controller.init(nativePtr);
126        return controller;
127    }
128
129    private void init(long nativePtr) {
130        mIoHandler = new Handler(mService.getServiceLooper());
131        mControlHandler = new Handler(mService.getServiceLooper());
132        mNativePtr = nativePtr;
133    }
134
135    @ServiceThreadOnly
136    void addLocalDevice(int deviceType, HdmiCecLocalDevice device) {
137        assertRunOnServiceThread();
138        mLocalDevices.put(deviceType, device);
139    }
140
141    /**
142     * Allocate a new logical address of the given device type. Allocated
143     * address will be reported through {@link AllocateAddressCallback}.
144     *
145     * <p> Declared as package-private, accessed by {@link HdmiControlService} only.
146     *
147     * @param deviceType type of device to used to determine logical address
148     * @param preferredAddress a logical address preferred to be allocated.
149     *                         If sets {@link Constants#ADDR_UNREGISTERED}, scans
150     *                         the smallest logical address matched with the given device type.
151     *                         Otherwise, scan address will start from {@code preferredAddress}
152     * @param callback callback interface to report allocated logical address to caller
153     */
154    @ServiceThreadOnly
155    void allocateLogicalAddress(final int deviceType, final int preferredAddress,
156            final AllocateAddressCallback callback) {
157        assertRunOnServiceThread();
158
159        runOnIoThread(new Runnable() {
160            @Override
161            public void run() {
162                handleAllocateLogicalAddress(deviceType, preferredAddress, callback);
163            }
164        });
165    }
166
167    @IoThreadOnly
168    private void handleAllocateLogicalAddress(final int deviceType, int preferredAddress,
169            final AllocateAddressCallback callback) {
170        assertRunOnIoThread();
171        int startAddress = preferredAddress;
172        // If preferred address is "unregistered", start address will be the smallest
173        // address matched with the given device type.
174        if (preferredAddress == Constants.ADDR_UNREGISTERED) {
175            for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
176                if (deviceType == HdmiUtils.getTypeFromAddress(i)) {
177                    startAddress = i;
178                    break;
179                }
180            }
181        }
182
183        int logicalAddress = Constants.ADDR_UNREGISTERED;
184        // Iterates all possible addresses which has the same device type.
185        for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
186            int curAddress = (startAddress + i) % NUM_LOGICAL_ADDRESS;
187            if (curAddress != Constants.ADDR_UNREGISTERED
188                    && deviceType == HdmiUtils.getTypeFromAddress(curAddress)) {
189                int failedPollingCount = 0;
190                for (int j = 0; j < HdmiConfig.ADDRESS_ALLOCATION_RETRY; ++j) {
191                    if (!sendPollMessage(curAddress, curAddress, 1)) {
192                        failedPollingCount++;
193                    }
194                }
195
196                // Pick logical address if failed ratio is more than a half of all retries.
197                if (failedPollingCount * 2 >  HdmiConfig.ADDRESS_ALLOCATION_RETRY) {
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    @ServiceThreadOnly
249    int addLogicalAddress(int newLogicalAddress) {
250        assertRunOnServiceThread();
251        if (HdmiUtils.isValidAddress(newLogicalAddress)) {
252            return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
253        } else {
254            return -1;
255        }
256    }
257
258    /**
259     * Clear all logical addresses registered in the device.
260     *
261     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
262     */
263    @ServiceThreadOnly
264    void clearLogicalAddress() {
265        assertRunOnServiceThread();
266        for (int i = 0; i < mLocalDevices.size(); ++i) {
267            mLocalDevices.valueAt(i).clearAddress();
268        }
269        nativeClearLogicalAddress(mNativePtr);
270    }
271
272    @ServiceThreadOnly
273    void clearLocalDevices() {
274        assertRunOnServiceThread();
275        mLocalDevices.clear();
276    }
277
278    /**
279     * Return the physical address of the device.
280     *
281     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
282     *
283     * @return CEC physical address of the device. The range of success address
284     *         is between 0x0000 and 0xFFFF. If failed it returns -1
285     */
286    @ServiceThreadOnly
287    int getPhysicalAddress() {
288        assertRunOnServiceThread();
289        return nativeGetPhysicalAddress(mNativePtr);
290    }
291
292    /**
293     * Return CEC version of the device.
294     *
295     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
296     */
297    @ServiceThreadOnly
298    int getVersion() {
299        assertRunOnServiceThread();
300        return nativeGetVersion(mNativePtr);
301    }
302
303    /**
304     * Return vendor id of the device.
305     *
306     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
307     */
308    @ServiceThreadOnly
309    int getVendorId() {
310        assertRunOnServiceThread();
311        return nativeGetVendorId(mNativePtr);
312    }
313
314    /**
315     * Set an option to CEC HAL.
316     *
317     * @param flag key of option
318     * @param value value of option
319     */
320    @ServiceThreadOnly
321    void setOption(int flag, int value) {
322        assertRunOnServiceThread();
323        nativeSetOption(mNativePtr, flag, value);
324    }
325
326    /**
327     * Configure ARC circuit in the hardware logic to start or stop the feature.
328     *
329     * @param enabled whether to enable/disable ARC
330     */
331    @ServiceThreadOnly
332    void setAudioReturnChannel(boolean enabled) {
333        assertRunOnServiceThread();
334        nativeSetAudioReturnChannel(mNativePtr, enabled);
335    }
336
337    /**
338     * Return the connection status of the specified port
339     *
340     * @param port port number to check connection status
341     * @return true if connected; otherwise, return false
342     */
343    @ServiceThreadOnly
344    boolean isConnected(int port) {
345        assertRunOnServiceThread();
346        return nativeIsConnected(mNativePtr, port);
347    }
348
349    /**
350     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
351     * devices.
352     *
353     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
354     *
355     * @param callback an interface used to get a list of all remote devices' address
356     * @param sourceAddress a logical address of source device where sends polling message
357     * @param pickStrategy strategy how to pick polling candidates
358     * @param retryCount the number of retry used to send polling message to remote devices
359     */
360    @ServiceThreadOnly
361    void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy,
362            int retryCount) {
363        assertRunOnServiceThread();
364
365        // Extract polling candidates. No need to poll against local devices.
366        List<Integer> pollingCandidates = pickPollCandidates(pickStrategy);
367        runDevicePolling(sourceAddress, pollingCandidates, retryCount, callback);
368    }
369
370    /**
371     * Return a list of all {@link HdmiCecLocalDevice}s.
372     *
373     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
374     */
375    @ServiceThreadOnly
376    List<HdmiCecLocalDevice> getLocalDeviceList() {
377        assertRunOnServiceThread();
378        return HdmiUtils.sparseArrayToList(mLocalDevices);
379    }
380
381    private List<Integer> pickPollCandidates(int pickStrategy) {
382        int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK;
383        Predicate<Integer> pickPredicate = null;
384        switch (strategy) {
385            case Constants.POLL_STRATEGY_SYSTEM_AUDIO:
386                pickPredicate = mSystemAudioAddressPredicate;
387                break;
388            case Constants.POLL_STRATEGY_REMOTES_DEVICES:
389            default:  // The default is POLL_STRATEGY_REMOTES_DEVICES.
390                pickPredicate = mRemoteDeviceAddressPredicate;
391                break;
392        }
393
394        int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK;
395        ArrayList<Integer> pollingCandidates = new ArrayList<>();
396        switch (iterationStrategy) {
397            case Constants.POLL_ITERATION_IN_ORDER:
398                for (int i = Constants.ADDR_TV; i <= Constants.ADDR_SPECIFIC_USE; ++i) {
399                    if (pickPredicate.apply(i)) {
400                        pollingCandidates.add(i);
401                    }
402                }
403                break;
404            case Constants.POLL_ITERATION_REVERSE_ORDER:
405            default:  // The default is reverse order.
406                for (int i = Constants.ADDR_SPECIFIC_USE; i >= Constants.ADDR_TV; --i) {
407                    if (pickPredicate.apply(i)) {
408                        pollingCandidates.add(i);
409                    }
410                }
411                break;
412        }
413        return pollingCandidates;
414    }
415
416    @ServiceThreadOnly
417    private boolean isAllocatedLocalDeviceAddress(int address) {
418        assertRunOnServiceThread();
419        for (int i = 0; i < mLocalDevices.size(); ++i) {
420            if (mLocalDevices.valueAt(i).isAddressOf(address)) {
421                return true;
422            }
423        }
424        return false;
425    }
426
427    @ServiceThreadOnly
428    private void runDevicePolling(final int sourceAddress,
429            final List<Integer> candidates, final int retryCount,
430            final DevicePollingCallback callback) {
431        assertRunOnServiceThread();
432        runOnIoThread(new Runnable() {
433            @Override
434            public void run() {
435                final ArrayList<Integer> allocated = new ArrayList<>();
436                for (Integer address : candidates) {
437                    if (sendPollMessage(sourceAddress, address, retryCount)) {
438                        allocated.add(address);
439                    }
440                }
441                if (callback != null) {
442                    runOnServiceThread(new Runnable() {
443                        @Override
444                        public void run() {
445                            callback.onPollingFinished(allocated);
446                        }
447                    });
448                }
449            }
450        });
451    }
452
453    @IoThreadOnly
454    private boolean sendPollMessage(int sourceAddress, int destinationAddress, int retryCount) {
455        assertRunOnIoThread();
456        for (int i = 0; i < retryCount; ++i) {
457            // <Polling Message> is a message which has empty body.
458            // If sending <Polling Message> failed (NAK), it becomes
459            // new logical address for the device because no device uses
460            // it as logical address of the device.
461            if (nativeSendCecCommand(mNativePtr, sourceAddress, destinationAddress, EMPTY_BODY)
462                    == Constants.SEND_RESULT_SUCCESS) {
463                return true;
464            }
465        }
466        return false;
467    }
468
469    private void assertRunOnIoThread() {
470        if (Looper.myLooper() != mIoHandler.getLooper()) {
471            throw new IllegalStateException("Should run on io thread.");
472        }
473    }
474
475    private void assertRunOnServiceThread() {
476        if (Looper.myLooper() != mControlHandler.getLooper()) {
477            throw new IllegalStateException("Should run on service thread.");
478        }
479    }
480
481    // Run a Runnable on IO thread.
482    // It should be careful to access member variables on IO thread because
483    // it can be accessed from system thread as well.
484    private void runOnIoThread(Runnable runnable) {
485        mIoHandler.post(runnable);
486    }
487
488    private void runOnServiceThread(Runnable runnable) {
489        mControlHandler.post(runnable);
490    }
491
492    private boolean isAcceptableAddress(int address) {
493        // Can access command targeting devices available in local device or broadcast command.
494        if (address == Constants.ADDR_BROADCAST) {
495            return true;
496        }
497        return isAllocatedLocalDeviceAddress(address);
498    }
499
500    @ServiceThreadOnly
501    private void onReceiveCommand(HdmiCecMessage message) {
502        assertRunOnServiceThread();
503        if (isAcceptableAddress(message.getDestination()) && mService.handleCecCommand(message)) {
504            return;
505        }
506        // Not handled message, so we will reply it with <Feature Abort>.
507        maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
508    }
509
510    @ServiceThreadOnly
511    void maySendFeatureAbortCommand(HdmiCecMessage message, int reason) {
512        assertRunOnServiceThread();
513        // Swap the source and the destination.
514        int src = message.getDestination();
515        int dest = message.getSource();
516        if (src == Constants.ADDR_BROADCAST || dest == Constants.ADDR_UNREGISTERED) {
517            // Don't reply <Feature Abort> from the unregistered devices or for the broadcasted
518            // messages. See CEC 12.2 Protocol General Rules for detail.
519            return;
520        }
521        int originalOpcode = message.getOpcode();
522        if (originalOpcode == Constants.MESSAGE_FEATURE_ABORT) {
523            return;
524        }
525        sendCommand(
526                HdmiCecMessageBuilder.buildFeatureAbortCommand(src, dest, originalOpcode, reason));
527    }
528
529    @ServiceThreadOnly
530    void sendCommand(HdmiCecMessage cecMessage) {
531        assertRunOnServiceThread();
532        sendCommand(cecMessage, null);
533    }
534
535    @ServiceThreadOnly
536    void sendCommand(final HdmiCecMessage cecMessage,
537            final HdmiControlService.SendMessageCallback callback) {
538        assertRunOnServiceThread();
539        runOnIoThread(new Runnable() {
540            @Override
541            public void run() {
542                byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
543                int i = 0;
544                int errorCode = Constants.SEND_RESULT_SUCCESS;
545                do {
546                    errorCode = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
547                            cecMessage.getDestination(), body);
548                    if (errorCode == Constants.SEND_RESULT_SUCCESS) {
549                        break;
550                    }
551                } while (i++ < HdmiConfig.RETRANSMISSION_COUNT);
552
553                final int finalError = errorCode;
554                if (finalError != Constants.SEND_RESULT_SUCCESS) {
555                    Slog.w(TAG, "Failed to send " + cecMessage);
556                }
557                if (callback != null) {
558                    runOnServiceThread(new Runnable() {
559                        @Override
560                        public void run() {
561                            callback.onSendCompleted(finalError);
562                        }
563                    });
564                }
565            }
566        });
567    }
568
569    /**
570     * Called by native when incoming CEC message arrived.
571     */
572    @ServiceThreadOnly
573    private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
574        assertRunOnServiceThread();
575        onReceiveCommand(HdmiCecMessageBuilder.of(srcAddress, dstAddress, body));
576    }
577
578    /**
579     * Called by native when a hotplug event issues.
580     */
581    @ServiceThreadOnly
582    private void handleHotplug(int port, boolean connected) {
583        assertRunOnServiceThread();
584        mService.onHotplug(port, connected);
585    }
586
587    private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
588    private static native int nativeSendCecCommand(long controllerPtr, int srcAddress,
589            int dstAddress, byte[] body);
590    private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress);
591    private static native void nativeClearLogicalAddress(long controllerPtr);
592    private static native int nativeGetPhysicalAddress(long controllerPtr);
593    private static native int nativeGetVersion(long controllerPtr);
594    private static native int nativeGetVendorId(long controllerPtr);
595    private static native HdmiPortInfo[] nativeGetPortInfos(long controllerPtr);
596    private static native void nativeSetOption(long controllerPtr, int flag, int value);
597    private static native void nativeSetAudioReturnChannel(long controllerPtr, boolean flag);
598    private static native boolean nativeIsConnected(long controllerPtr, int port);
599}
600