HdmiCecController.java revision f17024873b10dabed069e502030dd85d6257c0c4
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.IndentingPrintWriter;
27import com.android.internal.util.Predicate;
28import com.android.server.hdmi.HdmiAnnotations.IoThreadOnly;
29import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
30import com.android.server.hdmi.HdmiControlService.DevicePollingCallback;
31
32import libcore.util.EmptyArray;
33
34import java.util.ArrayList;
35import java.util.LinkedList;
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 Constants#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    private static final int NUM_LOGICAL_ADDRESS = 16;
71
72    // Predicate for whether the given logical address is remote device's one or not.
73    private final Predicate<Integer> mRemoteDeviceAddressPredicate = new Predicate<Integer>() {
74        @Override
75        public boolean apply(Integer address) {
76            return !isAllocatedLocalDeviceAddress(address);
77        }
78    };
79
80    // Predicate whether the given logical address is system audio's one or not
81    private final Predicate<Integer> mSystemAudioAddressPredicate = new Predicate<Integer>() {
82        @Override
83        public boolean apply(Integer address) {
84            return HdmiUtils.getTypeFromAddress(address) == Constants.ADDR_AUDIO_SYSTEM;
85        }
86    };
87
88    // Handler instance to process synchronous I/O (mainly send) message.
89    private Handler mIoHandler;
90
91    // Handler instance to process various messages coming from other CEC
92    // device or issued by internal state change.
93    private Handler mControlHandler;
94
95    // Stores the pointer to the native implementation of the service that
96    // interacts with HAL.
97    private volatile long mNativePtr;
98
99    private final HdmiControlService mService;
100
101    // Stores the local CEC devices in the system. Device type is used for key.
102    private final SparseArray<HdmiCecLocalDevice> mLocalDevices = new SparseArray<>();
103
104    // Private constructor.  Use HdmiCecController.create().
105    private HdmiCecController(HdmiControlService service) {
106        mService = service;
107    }
108
109    /**
110     * A factory method to get {@link HdmiCecController}. If it fails to initialize
111     * inner device or has no device it will return {@code null}.
112     *
113     * <p>Declared as package-private, accessed by {@link HdmiControlService} only.
114     * @param service {@link HdmiControlService} instance used to create internal handler
115     *                and to pass callback for incoming message or event.
116     * @return {@link HdmiCecController} if device is initialized successfully. Otherwise,
117     *         returns {@code null}.
118     */
119    static HdmiCecController create(HdmiControlService service) {
120        HdmiCecController controller = new HdmiCecController(service);
121        long nativePtr = nativeInit(controller, service.getServiceLooper().getQueue());
122        if (nativePtr == 0L) {
123            controller = null;
124            return null;
125        }
126
127        controller.init(nativePtr);
128        return controller;
129    }
130
131    private void init(long nativePtr) {
132        mIoHandler = new Handler(mService.getIoLooper());
133        mControlHandler = new Handler(mService.getServiceLooper());
134        mNativePtr = nativePtr;
135    }
136
137    @ServiceThreadOnly
138    void addLocalDevice(int deviceType, HdmiCecLocalDevice device) {
139        assertRunOnServiceThread();
140        mLocalDevices.put(deviceType, device);
141    }
142
143    /**
144     * Allocate a new logical address of the given device type. Allocated
145     * address will be reported through {@link AllocateAddressCallback}.
146     *
147     * <p> Declared as package-private, accessed by {@link HdmiControlService} only.
148     *
149     * @param deviceType type of device to used to determine logical address
150     * @param preferredAddress a logical address preferred to be allocated.
151     *                         If sets {@link Constants#ADDR_UNREGISTERED}, scans
152     *                         the smallest logical address matched with the given device type.
153     *                         Otherwise, scan address will start from {@code preferredAddress}
154     * @param callback callback interface to report allocated logical address to caller
155     */
156    @ServiceThreadOnly
157    void allocateLogicalAddress(final int deviceType, final int preferredAddress,
158            final AllocateAddressCallback callback) {
159        assertRunOnServiceThread();
160
161        runOnIoThread(new Runnable() {
162            @Override
163            public void run() {
164                handleAllocateLogicalAddress(deviceType, preferredAddress, callback);
165            }
166        });
167    }
168
169    @IoThreadOnly
170    private void handleAllocateLogicalAddress(final int deviceType, int preferredAddress,
171            final AllocateAddressCallback callback) {
172        assertRunOnIoThread();
173        int startAddress = preferredAddress;
174        // If preferred address is "unregistered", start address will be the smallest
175        // address matched with the given device type.
176        if (preferredAddress == Constants.ADDR_UNREGISTERED) {
177            for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
178                if (deviceType == HdmiUtils.getTypeFromAddress(i)) {
179                    startAddress = i;
180                    break;
181                }
182            }
183        }
184
185        int logicalAddress = Constants.ADDR_UNREGISTERED;
186        // Iterates all possible addresses which has the same device type.
187        for (int i = 0; i < NUM_LOGICAL_ADDRESS; ++i) {
188            int curAddress = (startAddress + i) % NUM_LOGICAL_ADDRESS;
189            if (curAddress != Constants.ADDR_UNREGISTERED
190                    && deviceType == HdmiUtils.getTypeFromAddress(curAddress)) {
191                int failedPollingCount = 0;
192                for (int j = 0; j < HdmiConfig.ADDRESS_ALLOCATION_RETRY; ++j) {
193                    if (!sendPollMessage(curAddress, curAddress, 1)) {
194                        failedPollingCount++;
195                    }
196                }
197
198                // Pick logical address if failed ratio is more than a half of all retries.
199                if (failedPollingCount * 2 >  HdmiConfig.ADDRESS_ALLOCATION_RETRY) {
200                    logicalAddress = curAddress;
201                    break;
202                }
203            }
204        }
205
206        final int assignedAddress = logicalAddress;
207        HdmiLogger.debug("New logical address for device [%d]: [preferred:%d, assigned:%d]",
208                        deviceType, preferredAddress, assignedAddress);
209        if (callback != null) {
210            runOnServiceThread(new Runnable() {
211                @Override
212                public void run() {
213                    callback.onAllocated(deviceType, assignedAddress);
214                }
215            });
216        }
217    }
218
219    private static byte[] buildBody(int opcode, byte[] params) {
220        byte[] body = new byte[params.length + 1];
221        body[0] = (byte) opcode;
222        System.arraycopy(params, 0, body, 1, params.length);
223        return body;
224    }
225
226
227    HdmiPortInfo[] getPortInfos() {
228        return nativeGetPortInfos(mNativePtr);
229    }
230
231    /**
232     * Return the locally hosted logical device of a given type.
233     *
234     * @param deviceType logical device type
235     * @return {@link HdmiCecLocalDevice} instance if the instance of the type is available;
236     *          otherwise null.
237     */
238    HdmiCecLocalDevice getLocalDevice(int deviceType) {
239        return mLocalDevices.get(deviceType);
240    }
241
242    /**
243     * Add a new logical address to the device. Device's HW should be notified
244     * when a new logical address is assigned to a device, so that it can accept
245     * a command having available destinations.
246     *
247     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
248     *
249     * @param newLogicalAddress a logical address to be added
250     * @return 0 on success. Otherwise, returns negative value
251     */
252    @ServiceThreadOnly
253    int addLogicalAddress(int newLogicalAddress) {
254        assertRunOnServiceThread();
255        if (HdmiUtils.isValidAddress(newLogicalAddress)) {
256            return nativeAddLogicalAddress(mNativePtr, newLogicalAddress);
257        } else {
258            return -1;
259        }
260    }
261
262    /**
263     * Clear all logical addresses registered in the device.
264     *
265     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
266     */
267    @ServiceThreadOnly
268    void clearLogicalAddress() {
269        assertRunOnServiceThread();
270        for (int i = 0; i < mLocalDevices.size(); ++i) {
271            mLocalDevices.valueAt(i).clearAddress();
272        }
273        nativeClearLogicalAddress(mNativePtr);
274    }
275
276    @ServiceThreadOnly
277    void clearLocalDevices() {
278        assertRunOnServiceThread();
279        mLocalDevices.clear();
280    }
281
282    /**
283     * Return the physical address of the device.
284     *
285     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
286     *
287     * @return CEC physical address of the device. The range of success address
288     *         is between 0x0000 and 0xFFFF. If failed it returns -1
289     */
290    @ServiceThreadOnly
291    int getPhysicalAddress() {
292        assertRunOnServiceThread();
293        return nativeGetPhysicalAddress(mNativePtr);
294    }
295
296    /**
297     * Return CEC version of the device.
298     *
299     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
300     */
301    @ServiceThreadOnly
302    int getVersion() {
303        assertRunOnServiceThread();
304        return nativeGetVersion(mNativePtr);
305    }
306
307    /**
308     * Return vendor id of the device.
309     *
310     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
311     */
312    @ServiceThreadOnly
313    int getVendorId() {
314        assertRunOnServiceThread();
315        return nativeGetVendorId(mNativePtr);
316    }
317
318    /**
319     * Set an option to CEC HAL.
320     *
321     * @param flag key of option
322     * @param value value of option
323     */
324    @ServiceThreadOnly
325    void setOption(int flag, int value) {
326        assertRunOnServiceThread();
327        HdmiLogger.debug("setOption: [flag:%d, value:%d]", flag, value);
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 sourceAddress a logical address of source device where sends polling message
362     * @param pickStrategy strategy how to pick polling candidates
363     * @param retryCount the number of retry used to send polling message to remote devices
364     */
365    @ServiceThreadOnly
366    void pollDevices(DevicePollingCallback callback, int sourceAddress, int pickStrategy,
367            int retryCount) {
368        assertRunOnServiceThread();
369
370        // Extract polling candidates. No need to poll against local devices.
371        List<Integer> pollingCandidates = pickPollCandidates(pickStrategy);
372        ArrayList<Integer> allocated = new ArrayList<>();
373        runDevicePolling(sourceAddress, pollingCandidates, retryCount, callback, allocated);
374    }
375
376    /**
377     * Return a list of all {@link HdmiCecLocalDevice}s.
378     *
379     * <p>Declared as package-private. accessed by {@link HdmiControlService} only.
380     */
381    @ServiceThreadOnly
382    List<HdmiCecLocalDevice> getLocalDeviceList() {
383        assertRunOnServiceThread();
384        return HdmiUtils.sparseArrayToList(mLocalDevices);
385    }
386
387    private List<Integer> pickPollCandidates(int pickStrategy) {
388        int strategy = pickStrategy & Constants.POLL_STRATEGY_MASK;
389        Predicate<Integer> pickPredicate = null;
390        switch (strategy) {
391            case Constants.POLL_STRATEGY_SYSTEM_AUDIO:
392                pickPredicate = mSystemAudioAddressPredicate;
393                break;
394            case Constants.POLL_STRATEGY_REMOTES_DEVICES:
395            default:  // The default is POLL_STRATEGY_REMOTES_DEVICES.
396                pickPredicate = mRemoteDeviceAddressPredicate;
397                break;
398        }
399
400        int iterationStrategy = pickStrategy & Constants.POLL_ITERATION_STRATEGY_MASK;
401        LinkedList<Integer> pollingCandidates = new LinkedList<>();
402        switch (iterationStrategy) {
403            case Constants.POLL_ITERATION_IN_ORDER:
404                for (int i = Constants.ADDR_TV; i <= Constants.ADDR_SPECIFIC_USE; ++i) {
405                    if (pickPredicate.apply(i)) {
406                        pollingCandidates.add(i);
407                    }
408                }
409                break;
410            case Constants.POLL_ITERATION_REVERSE_ORDER:
411            default:  // The default is reverse order.
412                for (int i = Constants.ADDR_SPECIFIC_USE; i >= Constants.ADDR_TV; --i) {
413                    if (pickPredicate.apply(i)) {
414                        pollingCandidates.add(i);
415                    }
416                }
417                break;
418        }
419        return pollingCandidates;
420    }
421
422    @ServiceThreadOnly
423    private boolean isAllocatedLocalDeviceAddress(int address) {
424        assertRunOnServiceThread();
425        for (int i = 0; i < mLocalDevices.size(); ++i) {
426            if (mLocalDevices.valueAt(i).isAddressOf(address)) {
427                return true;
428            }
429        }
430        return false;
431    }
432
433    @ServiceThreadOnly
434    private void runDevicePolling(final int sourceAddress,
435            final List<Integer> candidates, final int retryCount,
436            final DevicePollingCallback callback, final List<Integer> allocated) {
437        assertRunOnServiceThread();
438        if (candidates.isEmpty()) {
439            if (callback != null) {
440                HdmiLogger.debug("[P]:AllocatedAddress=%s", allocated.toString());
441                callback.onPollingFinished(allocated);
442            }
443            return;
444        }
445
446        final Integer candidate = candidates.remove(0);
447        // Proceed polling action for the next address once polling action for the
448        // previous address is done.
449        runOnIoThread(new Runnable() {
450            @Override
451            public void run() {
452                if (sendPollMessage(sourceAddress, candidate, retryCount)) {
453                    allocated.add(candidate);
454                }
455                runOnServiceThread(new Runnable() {
456                    @Override
457                    public void run() {
458                        runDevicePolling(sourceAddress, candidates, retryCount, callback,
459                                allocated);
460                    }
461                });
462            }
463        });
464    }
465
466    @IoThreadOnly
467    private boolean sendPollMessage(int sourceAddress, int destinationAddress, int retryCount) {
468        assertRunOnIoThread();
469        for (int i = 0; i < retryCount; ++i) {
470            // <Polling Message> is a message which has empty body.
471            // If sending <Polling Message> failed (NAK), it becomes
472            // new logical address for the device because no device uses
473            // it as logical address of the device.
474            if (nativeSendCecCommand(mNativePtr, sourceAddress, destinationAddress, EMPTY_BODY)
475                    == Constants.SEND_RESULT_SUCCESS) {
476                return true;
477            }
478        }
479        return false;
480    }
481
482    private void assertRunOnIoThread() {
483        if (Looper.myLooper() != mIoHandler.getLooper()) {
484            throw new IllegalStateException("Should run on io thread.");
485        }
486    }
487
488    private void assertRunOnServiceThread() {
489        if (Looper.myLooper() != mControlHandler.getLooper()) {
490            throw new IllegalStateException("Should run on service thread.");
491        }
492    }
493
494    // Run a Runnable on IO thread.
495    // It should be careful to access member variables on IO thread because
496    // it can be accessed from system thread as well.
497    private void runOnIoThread(Runnable runnable) {
498        mIoHandler.post(runnable);
499    }
500
501    private void runOnServiceThread(Runnable runnable) {
502        mControlHandler.post(runnable);
503    }
504
505    @ServiceThreadOnly
506    void flush(final Runnable runnable) {
507        assertRunOnServiceThread();
508        runOnIoThread(new Runnable() {
509            @Override
510            public void run() {
511                // This ensures the runnable for cleanup is performed after all the pending
512                // commands are processed by IO thread.
513                runOnServiceThread(runnable);
514            }
515        });
516    }
517
518    private boolean isAcceptableAddress(int address) {
519        // Can access command targeting devices available in local device or broadcast command.
520        if (address == Constants.ADDR_BROADCAST) {
521            return true;
522        }
523        return isAllocatedLocalDeviceAddress(address);
524    }
525
526    @ServiceThreadOnly
527    private void onReceiveCommand(HdmiCecMessage message) {
528        assertRunOnServiceThread();
529        if (isAcceptableAddress(message.getDestination()) && mService.handleCecCommand(message)) {
530            return;
531        }
532        // Not handled message, so we will reply it with <Feature Abort>.
533        maySendFeatureAbortCommand(message, Constants.ABORT_UNRECOGNIZED_OPCODE);
534    }
535
536    @ServiceThreadOnly
537    void maySendFeatureAbortCommand(HdmiCecMessage message, int reason) {
538        assertRunOnServiceThread();
539        // Swap the source and the destination.
540        int src = message.getDestination();
541        int dest = message.getSource();
542        if (src == Constants.ADDR_BROADCAST || dest == Constants.ADDR_UNREGISTERED) {
543            // Don't reply <Feature Abort> from the unregistered devices or for the broadcasted
544            // messages. See CEC 12.2 Protocol General Rules for detail.
545            return;
546        }
547        int originalOpcode = message.getOpcode();
548        if (originalOpcode == Constants.MESSAGE_FEATURE_ABORT) {
549            return;
550        }
551        sendCommand(
552                HdmiCecMessageBuilder.buildFeatureAbortCommand(src, dest, originalOpcode, reason));
553    }
554
555    @ServiceThreadOnly
556    void sendCommand(HdmiCecMessage cecMessage) {
557        assertRunOnServiceThread();
558        sendCommand(cecMessage, null);
559    }
560
561    @ServiceThreadOnly
562    void sendCommand(final HdmiCecMessage cecMessage,
563            final HdmiControlService.SendMessageCallback callback) {
564        assertRunOnServiceThread();
565        runOnIoThread(new Runnable() {
566            @Override
567            public void run() {
568                HdmiLogger.debug("[S]:" + cecMessage);
569                byte[] body = buildBody(cecMessage.getOpcode(), cecMessage.getParams());
570                int i = 0;
571                int errorCode = Constants.SEND_RESULT_SUCCESS;
572                do {
573                    errorCode = nativeSendCecCommand(mNativePtr, cecMessage.getSource(),
574                            cecMessage.getDestination(), body);
575                    if (errorCode == Constants.SEND_RESULT_SUCCESS) {
576                        break;
577                    }
578                } while (i++ < HdmiConfig.RETRANSMISSION_COUNT);
579
580                final int finalError = errorCode;
581                if (finalError != Constants.SEND_RESULT_SUCCESS) {
582                    Slog.w(TAG, "Failed to send " + cecMessage);
583                }
584                if (callback != null) {
585                    runOnServiceThread(new Runnable() {
586                        @Override
587                        public void run() {
588                            callback.onSendCompleted(finalError);
589                        }
590                    });
591                }
592            }
593        });
594    }
595
596    /**
597     * Called by native when incoming CEC message arrived.
598     */
599    @ServiceThreadOnly
600    private void handleIncomingCecCommand(int srcAddress, int dstAddress, byte[] body) {
601        assertRunOnServiceThread();
602        HdmiCecMessage command = HdmiCecMessageBuilder.of(srcAddress, dstAddress, body);
603        HdmiLogger.debug("[R]:" + command);
604        onReceiveCommand(command);
605    }
606
607    /**
608     * Called by native when a hotplug event issues.
609     */
610    @ServiceThreadOnly
611    private void handleHotplug(int port, boolean connected) {
612        assertRunOnServiceThread();
613        HdmiLogger.debug("Hotplug event:[port:%d, connected:%b]", port, connected);
614        mService.onHotplug(port, connected);
615    }
616
617    void dump(final IndentingPrintWriter pw) {
618        for (int i = 0; i < mLocalDevices.size(); ++i) {
619            pw.println("HdmiCecLocalDevice #" + i + ":");
620            pw.increaseIndent();
621            mLocalDevices.valueAt(i).dump(pw);
622            pw.decreaseIndent();
623        }
624    }
625
626    private static native long nativeInit(HdmiCecController handler, MessageQueue messageQueue);
627    private static native int nativeSendCecCommand(long controllerPtr, int srcAddress,
628            int dstAddress, byte[] body);
629    private static native int nativeAddLogicalAddress(long controllerPtr, int logicalAddress);
630    private static native void nativeClearLogicalAddress(long controllerPtr);
631    private static native int nativeGetPhysicalAddress(long controllerPtr);
632    private static native int nativeGetVersion(long controllerPtr);
633    private static native int nativeGetVendorId(long controllerPtr);
634    private static native HdmiPortInfo[] nativeGetPortInfos(long controllerPtr);
635    private static native void nativeSetOption(long controllerPtr, int flag, int value);
636    private static native void nativeSetAudioReturnChannel(long controllerPtr, boolean flag);
637    private static native boolean nativeIsConnected(long controllerPtr, int port);
638}
639