HdmiControlService.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.annotation.Nullable;
20import android.content.Context;
21import android.hardware.hdmi.HdmiCec;
22import android.hardware.hdmi.HdmiCecDeviceInfo;
23import android.hardware.hdmi.HdmiCecMessage;
24import android.hardware.hdmi.HdmiPortInfo;
25import android.hardware.hdmi.IHdmiControlCallback;
26import android.hardware.hdmi.IHdmiControlService;
27import android.hardware.hdmi.IHdmiHotplugEventListener;
28import android.os.Build;
29import android.os.Handler;
30import android.os.HandlerThread;
31import android.os.IBinder;
32import android.os.Looper;
33import android.os.RemoteException;
34import android.util.Slog;
35import android.util.SparseArray;
36import android.util.SparseIntArray;
37
38import com.android.internal.annotations.GuardedBy;
39import com.android.server.SystemService;
40import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
41import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback;
42
43import java.util.ArrayList;
44import java.util.Collections;
45import java.util.Iterator;
46import java.util.LinkedList;
47import java.util.List;
48
49/**
50 * Provides a service for sending and processing HDMI control messages,
51 * HDMI-CEC and MHL control command, and providing the information on both standard.
52 */
53public final class HdmiControlService extends SystemService {
54    private static final String TAG = "HdmiControlService";
55
56    // TODO: Rename the permission to HDMI_CONTROL.
57    private static final String PERMISSION = "android.permission.HDMI_CEC";
58
59    static final int SEND_RESULT_SUCCESS = 0;
60    static final int SEND_RESULT_NAK = -1;
61    static final int SEND_RESULT_FAILURE = -2;
62
63    static final int POLL_STRATEGY_MASK = 0x3;  // first and second bit.
64    static final int POLL_STRATEGY_REMOTES_DEVICES = 0x1;
65    static final int POLL_STRATEGY_SYSTEM_AUDIO = 0x2;
66
67    static final int POLL_ITERATION_STRATEGY_MASK = 0x30000;  // first and second bit.
68    static final int POLL_ITERATION_IN_ORDER = 0x10000;
69    static final int POLL_ITERATION_REVERSE_ORDER = 0x20000;
70
71    /**
72     * Interface to report send result.
73     */
74    interface SendMessageCallback {
75        /**
76         * Called when {@link HdmiControlService#sendCecCommand} is completed.
77         *
78         * @param error result of send request.
79         * @see {@link #SEND_RESULT_SUCCESS}
80         * @see {@link #SEND_RESULT_NAK}
81         * @see {@link #SEND_RESULT_FAILURE}
82         */
83        void onSendCompleted(int error);
84    }
85
86    /**
87     * Interface to get a list of available logical devices.
88     */
89    interface DevicePollingCallback {
90        /**
91         * Called when device polling is finished.
92         *
93         * @param ackedAddress a list of logical addresses of available devices
94         */
95        void onPollingFinished(List<Integer> ackedAddress);
96    }
97
98    // A thread to handle synchronous IO of CEC and MHL control service.
99    // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms)
100    // and sparse call it shares a thread to handle IO operations.
101    private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread");
102
103    // A collection of FeatureAction.
104    // Note that access to this collection should happen in service thread.
105    private final LinkedList<FeatureAction> mActions = new LinkedList<>();
106
107    // Used to synchronize the access to the service.
108    private final Object mLock = new Object();
109
110    // Type of logical devices hosted in the system. Stored in the unmodifiable list.
111    private final List<Integer> mLocalDevices;
112
113    // List of listeners registered by callers that want to get notified of
114    // hotplug events.
115    private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>();
116
117    // List of records for hotplug event listener to handle the the caller killed in action.
118    private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
119            new ArrayList<>();
120
121    // Handler running on service thread. It's used to run a task in service thread.
122    private final Handler mHandler = new Handler();
123
124    private final HdmiCecMessageCache mCecMessageCache = new HdmiCecMessageCache();
125
126    @Nullable
127    private HdmiCecController mCecController;
128
129    @Nullable
130    private HdmiMhlController mMhlController;
131
132    // HDMI port information. Stored in the unmodifiable list to keep the static information
133    // from being modified.
134    private List<HdmiPortInfo> mPortInfo;
135
136    // Logical address of the active source.
137    @GuardedBy("mLock")
138    private int mActiveSource;
139
140    // Active routing path. Physical address of the active source but not all the time, such as
141    // when the new active source does not claim itself to be one.
142    @GuardedBy("mLock")
143    private int mActiveRoutingPath;
144
145    // Set to true while the service is in normal mode. While set to false, no input change is
146    // allowed. Used for situations where input change can confuse users such as channel auto-scan,
147    // system upgrade, etc., a.k.a. "prohibit mode".
148    @GuardedBy("mLock")
149    private boolean mInputChangeEnabled;
150
151    @GuardedBy("mLock")
152    // Whether ARC is "enabled" or not.
153    // TODO: it may need to hold lock if it's accessed from others.
154    private boolean mArcStatusEnabled = false;
155
156    @GuardedBy("mLock")
157    // Whether SystemAudioMode is "On" or not.
158    private boolean mSystemAudioMode;
159
160    public HdmiControlService(Context context) {
161        super(context);
162        mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray(
163                com.android.internal.R.array.config_hdmiCecLogicalDeviceType));
164        // TODO: Get control flag from persistent storage
165        mInputChangeEnabled = true;
166    }
167
168    @Override
169    public void onStart() {
170        mIoThread.start();
171        mCecController = HdmiCecController.create(this);
172
173        if (mCecController != null) {
174            initializeLocalDevices(mLocalDevices);
175        } else {
176            Slog.i(TAG, "Device does not support HDMI-CEC.");
177        }
178
179        mMhlController = HdmiMhlController.create(this);
180        if (mMhlController == null) {
181            Slog.i(TAG, "Device does not support MHL-control.");
182        }
183        mPortInfo = initPortInfo();
184        publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService());
185
186        // TODO: Read the preference for SystemAudioMode and initialize mSystemAudioMode and
187        // start to monitor the preference value and invoke SystemAudioActionFromTv if needed.
188    }
189
190    private void initializeLocalDevices(final List<Integer> deviceTypes) {
191        // A container for [Logical Address, Local device info].
192        final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>();
193        final SparseIntArray finished = new SparseIntArray();
194        for (int type : deviceTypes) {
195            final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type);
196            localDevice.init();
197            mCecController.allocateLogicalAddress(type,
198                    localDevice.getPreferredAddress(), new AllocateAddressCallback() {
199                @Override
200                public void onAllocated(int deviceType, int logicalAddress) {
201                    if (logicalAddress == HdmiCec.ADDR_UNREGISTERED) {
202                        Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]");
203                    } else {
204                        HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType);
205                        localDevice.setDeviceInfo(deviceInfo);
206                        mCecController.addLocalDevice(deviceType, localDevice);
207                        mCecController.addLogicalAddress(logicalAddress);
208                        devices.append(logicalAddress, localDevice);
209                    }
210                    finished.append(deviceType, logicalAddress);
211
212                    // Once finish address allocation for all devices, notify
213                    // it to each device.
214                    if (deviceTypes.size() == finished.size()) {
215                        notifyAddressAllocated(devices);
216                    }
217                }
218            });
219        }
220    }
221
222    private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices) {
223        for (int i = 0; i < devices.size(); ++i) {
224            int address = devices.keyAt(i);
225            HdmiCecLocalDevice device = devices.valueAt(i);
226            device.onAddressAllocated(address);
227        }
228    }
229
230    // Initialize HDMI port information. Combine the information from CEC and MHL HAL and
231    // keep them in one place.
232    private List<HdmiPortInfo> initPortInfo() {
233        HdmiPortInfo[] cecPortInfo = null;
234
235        // CEC HAL provides majority of the info while MHL does only MHL support flag for
236        // each port. Return empty array if CEC HAL didn't provide the info.
237        if (mCecController != null) {
238            cecPortInfo = mCecController.getPortInfos();
239        }
240        if (cecPortInfo == null) {
241            return Collections.emptyList();
242        }
243
244        HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0];
245        if (mMhlController != null) {
246            // TODO: Implement plumbing logic to get MHL port information.
247            // mhlPortInfo = mMhlController.getPortInfos();
248        }
249
250        // Use the id (port number) to find the matched info between CEC and MHL to combine them
251        // into one. Leave the field `mhlSupported` to false if matched MHL entry is not found.
252        ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length);
253        for (int i = 0; i < cecPortInfo.length; ++i) {
254            HdmiPortInfo cec = cecPortInfo[i];
255            int id = cec.getId();
256            boolean mhlInfoFound = false;
257            for (HdmiPortInfo mhl : mhlPortInfo) {
258                if (id == mhl.getId()) {
259                    result.add(new HdmiPortInfo(id, cec.getType(), cec.getAddress(),
260                            cec.isCecSupported(), mhl.isMhlSupported(), cec.isArcSupported()));
261                    mhlInfoFound = true;
262                    break;
263                }
264            }
265            if (!mhlInfoFound) {
266                result.add(cec);
267            }
268        }
269
270        return Collections.unmodifiableList(result);
271    }
272
273    /**
274     * Returns HDMI port information for the given port id.
275     *
276     * @param portId HDMI port id
277     * @return {@link HdmiPortInfo} for the given port
278     */
279    HdmiPortInfo getPortInfo(int portId) {
280        // mPortInfo is an unmodifiable list and the only reference to its inner list.
281        // No lock is necessary.
282        for (HdmiPortInfo info : mPortInfo) {
283            if (portId == info.getId()) {
284                return info;
285            }
286        }
287        return null;
288    }
289
290    /**
291     * Returns {@link Looper} for IO operation.
292     *
293     * <p>Declared as package-private.
294     */
295    Looper getIoLooper() {
296        return mIoThread.getLooper();
297    }
298
299    /**
300     * Returns {@link Looper} of main thread. Use this {@link Looper} instance
301     * for tasks that are running on main service thread.
302     *
303     * <p>Declared as package-private.
304     */
305    Looper getServiceLooper() {
306        return mHandler.getLooper();
307    }
308
309    int getActiveSource() {
310        synchronized (mLock) {
311            return mActiveSource;
312        }
313    }
314
315    int getActivePath() {
316        synchronized (mLock) {
317            return mActiveRoutingPath;
318        }
319    }
320
321    /**
322     * Returns the path (physical address) of the device at the top of the currently active
323     * routing path. Used to get the corresponding port address of the HDMI input of the TV.
324     */
325    int getActiveInput() {
326        synchronized (mLock) {
327            return mActiveRoutingPath & HdmiConstants.ROUTING_PATH_TOP_MASK;
328        }
329    }
330
331    void updateActiveDevice(int logicalAddress, int physicalAddress) {
332        synchronized (mLock) {
333            mActiveSource = logicalAddress;
334            mActiveRoutingPath = physicalAddress;
335        }
336    }
337
338    void setInputChangeEnabled(boolean enabled) {
339        synchronized (mLock) {
340            mInputChangeEnabled = enabled;
341        }
342    }
343
344    /**
345     * Returns physical address of the device.
346     */
347    int getPhysicalAddress() {
348        return mCecController.getPhysicalAddress();
349    }
350
351    /**
352     * Returns vendor id of CEC service.
353     */
354    int getVendorId() {
355        return mCecController.getVendorId();
356    }
357
358    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
359        assertRunOnServiceThread();
360        return mCecController.getDeviceInfo(logicalAddress);
361    }
362
363    /**
364     * Returns version of CEC.
365     */
366    int getCecVersion() {
367        return mCecController.getVersion();
368    }
369
370    /**
371     * Returns a list of {@link HdmiCecDeviceInfo}.
372     *
373     * @param includeLocalDevice whether to include local devices
374     */
375    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
376        assertRunOnServiceThread();
377        return mCecController.getDeviceInfoList(includeLocalDevice);
378    }
379
380    /**
381     * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches
382     * the given routing path. CEC devices use routing path for its physical address to
383     * describe the hierarchy of the devices in the network.
384     *
385     * @param path routing path or physical address
386     * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null
387     */
388    HdmiCecDeviceInfo getDeviceInfoByPath(int path) {
389        assertRunOnServiceThread();
390        for (HdmiCecDeviceInfo info : mCecController.getDeviceInfoList(false)) {
391            if (info.getPhysicalAddress() == path) {
392                return info;
393            }
394        }
395        return null;
396    }
397
398    /**
399     * Add and start a new {@link FeatureAction} to the action queue.
400     *
401     * @param action {@link FeatureAction} to add and start
402     */
403    void addAndStartAction(final FeatureAction action) {
404        // TODO: may need to check the number of stale actions.
405        runOnServiceThread(new Runnable() {
406            @Override
407            public void run() {
408                mActions.add(action);
409                action.start();
410            }
411        });
412    }
413
414    void setSystemAudioMode(boolean on) {
415        synchronized (mLock) {
416            mSystemAudioMode = on;
417        }
418    }
419
420    boolean getSystemAudioMode() {
421        synchronized (mLock) {
422            return mSystemAudioMode;
423        }
424    }
425
426    // See if we have an action of a given type in progress.
427    <T extends FeatureAction> boolean hasAction(final Class<T> clazz) {
428        for (FeatureAction action : mActions) {
429            if (action.getClass().equals(clazz)) {
430                return true;
431            }
432        }
433        return false;
434    }
435
436    /**
437     * Remove the given {@link FeatureAction} object from the action queue.
438     *
439     * @param action {@link FeatureAction} to remove
440     */
441    void removeAction(final FeatureAction action) {
442        assertRunOnServiceThread();
443        mActions.remove(action);
444    }
445
446    // Remove all actions matched with the given Class type.
447    <T extends FeatureAction> void removeAction(final Class<T> clazz) {
448        removeActionExcept(clazz, null);
449    }
450
451    // Remove all actions matched with the given Class type besides |exception|.
452    <T extends FeatureAction> void removeActionExcept(final Class<T> clazz,
453            final FeatureAction exception) {
454        assertRunOnServiceThread();
455        Iterator<FeatureAction> iter = mActions.iterator();
456        while (iter.hasNext()) {
457            FeatureAction action = iter.next();
458            if (action != exception && action.getClass().equals(clazz)) {
459                action.clear();
460                mActions.remove(action);
461            }
462        }
463    }
464
465    private void runOnServiceThread(Runnable runnable) {
466        mHandler.post(runnable);
467    }
468
469    void runOnServiceThreadAtFrontOfQueue(Runnable runnable) {
470        mHandler.postAtFrontOfQueue(runnable);
471    }
472
473    private void assertRunOnServiceThread() {
474        if (Looper.myLooper() != mHandler.getLooper()) {
475            throw new IllegalStateException("Should run on service thread.");
476        }
477    }
478
479    /**
480     * Change ARC status into the given {@code enabled} status.
481     *
482     * @return {@code true} if ARC was in "Enabled" status
483     */
484    boolean setArcStatus(boolean enabled) {
485        assertRunOnServiceThread();
486        synchronized (mLock) {
487            boolean oldStatus = mArcStatusEnabled;
488            // 1. Enable/disable ARC circuit.
489            mCecController.setAudioReturnChannel(enabled);
490
491            // TODO: notify arc mode change to AudioManager.
492
493            // 2. Update arc status;
494            mArcStatusEnabled = enabled;
495            return oldStatus;
496        }
497    }
498
499    /**
500     * Transmit a CEC command to CEC bus.
501     *
502     * @param command CEC command to send out
503     * @param callback interface used to the result of send command
504     */
505    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
506        mCecController.sendCommand(command, callback);
507    }
508
509    void sendCecCommand(HdmiCecMessage command) {
510        mCecController.sendCommand(command, null);
511    }
512
513    boolean handleCecCommand(HdmiCecMessage message) {
514        // Cache incoming message. Note that it caches only white-listed one.
515        mCecMessageCache.cacheMessage(message);
516
517        // Commands that queries system information replies directly instead
518        // of creating FeatureAction because they are state-less.
519        // TODO: move the leftover message to local device.
520        switch (message.getOpcode()) {
521            case HdmiCec.MESSAGE_INITIATE_ARC:
522                handleInitiateArc(message);
523                return true;
524            case HdmiCec.MESSAGE_TERMINATE_ARC:
525                handleTerminateArc(message);
526                return true;
527            case HdmiCec.MESSAGE_SET_SYSTEM_AUDIO_MODE:
528                handleSetSystemAudioMode(message);
529                return true;
530            case HdmiCec.MESSAGE_SYSTEM_AUDIO_MODE_STATUS:
531                handleSystemAudioModeStatus(message);
532                return true;
533            default:
534                if (dispatchMessageToAction(message)) {
535                    return true;
536                }
537                break;
538        }
539
540        return dispatchMessageToLocalDevice(message);
541    }
542
543    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
544        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
545            if (device.dispatchMessage(message)) {
546                return true;
547            }
548        }
549        return false;
550    }
551
552    /**
553     * Called when a new hotplug event is issued.
554     *
555     * @param portNo hdmi port number where hot plug event issued.
556     * @param connected whether to be plugged in or not
557     */
558    void onHotplug(int portNo, boolean connected) {
559        // TODO: Start "RequestArcInitiationAction" if ARC port.
560    }
561
562    /**
563     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
564     * devices.
565     *
566     * @param callback an interface used to get a list of all remote devices' address
567     * @param pickStrategy strategy how to pick polling candidates
568     * @param retryCount the number of retry used to send polling message to remote devices
569     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
570     */
571    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
572        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
573    }
574
575    private int checkPollStrategy(int pickStrategy) {
576        int strategy = pickStrategy & POLL_STRATEGY_MASK;
577        if (strategy == 0) {
578            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
579        }
580        int iterationStrategy = pickStrategy & POLL_ITERATION_STRATEGY_MASK;
581        if (iterationStrategy == 0) {
582            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
583        }
584        return strategy | iterationStrategy;
585    }
586
587    /**
588     * Launch device discovery sequence. It starts with clearing the existing device info list.
589     * Note that it assumes that logical address of all local devices is already allocated.
590     *
591     * @param sourceAddress a logical address of tv
592     */
593    void launchDeviceDiscovery(final int sourceAddress) {
594        // At first, clear all existing device infos.
595        mCecController.clearDeviceInfoList();
596        // TODO: flush cec message cache when CEC is turned off.
597
598        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, sourceAddress,
599                new DeviceDiscoveryCallback() {
600                    @Override
601                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
602                        for (HdmiCecDeviceInfo info : deviceInfos) {
603                            addCecDevice(info);
604                        }
605
606                        // Add device info of all local devices.
607                        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
608                            addCecDevice(device.getDeviceInfo());
609                        }
610
611                        addAndStartAction(new HotplugDetectionAction(HdmiControlService.this,
612                                sourceAddress));
613                    }
614                });
615        addAndStartAction(action);
616    }
617
618    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
619        // TODO: find better name instead of model name.
620        String displayName = Build.MODEL;
621        return new HdmiCecDeviceInfo(logicalAddress,
622                getPhysicalAddress(), deviceType, getVendorId(), displayName);
623    }
624
625    private void handleInitiateArc(HdmiCecMessage message){
626        // In case where <Initiate Arc> is started by <Request ARC Initiation>
627        // need to clean up RequestArcInitiationAction.
628        removeAction(RequestArcInitiationAction.class);
629        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
630                message.getDestination(), message.getSource(), true);
631        addAndStartAction(action);
632    }
633
634    private void handleTerminateArc(HdmiCecMessage message) {
635        // In case where <Terminate Arc> is started by <Request ARC Termination>
636        // need to clean up RequestArcInitiationAction.
637        // TODO: check conditions of power status by calling is_connected api
638        // to be added soon.
639        removeAction(RequestArcTerminationAction.class);
640        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
641                message.getDestination(), message.getSource(), false);
642        addAndStartAction(action);
643    }
644
645    private boolean dispatchMessageToAction(HdmiCecMessage message) {
646        for (FeatureAction action : mActions) {
647            if (action.processCommand(message)) {
648                return true;
649            }
650        }
651        Slog.w(TAG, "Unsupported cec command:" + message);
652        return false;
653    }
654
655    private void handleSetSystemAudioMode(HdmiCecMessage message) {
656        if (dispatchMessageToAction(message) || !isMessageForSystemAudio(message)) {
657            return;
658        }
659        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
660                message.getDestination(), message.getSource(),
661                HdmiUtils.parseCommandParamSystemAudioStatus(message));
662        addAndStartAction(action);
663    }
664
665    private void handleSystemAudioModeStatus(HdmiCecMessage message) {
666        if (!isMessageForSystemAudio(message)) {
667            return;
668        }
669        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
670    }
671
672    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
673        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
674                || message.getDestination() != HdmiCec.ADDR_TV
675                || getAvrDeviceInfo() == null) {
676            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
677            return false;
678        }
679        return true;
680    }
681
682    // Record class that monitors the event of the caller of being killed. Used to clean up
683    // the listener list and record list accordingly.
684    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
685        private final IHdmiHotplugEventListener mListener;
686
687        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
688            mListener = listener;
689        }
690
691        @Override
692        public void binderDied() {
693            synchronized (mLock) {
694                mHotplugEventListenerRecords.remove(this);
695                mHotplugEventListeners.remove(mListener);
696            }
697        }
698    }
699
700    void addCecDevice(HdmiCecDeviceInfo info) {
701        mCecController.addDeviceInfo(info);
702    }
703
704    private void enforceAccessPermission() {
705        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
706    }
707
708    private final class BinderService extends IHdmiControlService.Stub {
709        @Override
710        public int[] getSupportedTypes() {
711            enforceAccessPermission();
712            // mLocalDevices is an unmodifiable list - no lock necesary.
713            int[] localDevices = new int[mLocalDevices.size()];
714            for (int i = 0; i < localDevices.length; ++i) {
715                localDevices[i] = mLocalDevices.get(i);
716            }
717            return localDevices;
718        }
719
720        @Override
721        public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) {
722            enforceAccessPermission();
723            runOnServiceThread(new Runnable() {
724                @Override
725                public void run() {
726                    HdmiCecLocalDeviceTv tv =
727                            (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV);
728                    if (tv == null) {
729                        Slog.w(TAG, "Local playback device not available");
730                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
731                        return;
732                    }
733                    tv.deviceSelect(logicalAddress, callback);
734                }
735            });
736        }
737
738
739        @Override
740        public void oneTouchPlay(final IHdmiControlCallback callback) {
741            enforceAccessPermission();
742            runOnServiceThread(new Runnable() {
743                @Override
744                public void run() {
745                    HdmiControlService.this.oneTouchPlay(callback);
746                }
747            });
748        }
749
750        @Override
751        public void queryDisplayStatus(final IHdmiControlCallback callback) {
752            enforceAccessPermission();
753            runOnServiceThread(new Runnable() {
754                @Override
755                public void run() {
756                    HdmiControlService.this.queryDisplayStatus(callback);
757                }
758            });
759        }
760
761        @Override
762        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
763            enforceAccessPermission();
764            runOnServiceThread(new Runnable() {
765                @Override
766                public void run() {
767                    HdmiControlService.this.addHotplugEventListener(listener);
768                }
769            });
770        }
771
772        @Override
773        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
774            enforceAccessPermission();
775            runOnServiceThread(new Runnable() {
776                @Override
777                public void run() {
778                    HdmiControlService.this.removeHotplugEventListener(listener);
779                }
780            });
781        }
782    }
783
784    private void oneTouchPlay(IHdmiControlCallback callback) {
785        if (hasAction(OneTouchPlayAction.class)) {
786            Slog.w(TAG, "oneTouchPlay already in progress");
787            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
788            return;
789        }
790        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
791        if (source == null) {
792            Slog.w(TAG, "Local playback device not available");
793            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
794            return;
795        }
796        // TODO: Consider the case of multiple TV sets. For now we always direct the command
797        //       to the primary one.
798        OneTouchPlayAction action = OneTouchPlayAction.create(this,
799                source.getDeviceInfo().getLogicalAddress(),
800                source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback);
801        if (action == null) {
802            Slog.w(TAG, "Cannot initiate oneTouchPlay");
803            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
804            return;
805        }
806        addAndStartAction(action);
807    }
808
809    private void queryDisplayStatus(IHdmiControlCallback callback) {
810        if (hasAction(DevicePowerStatusAction.class)) {
811            Slog.w(TAG, "queryDisplayStatus already in progress");
812            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
813            return;
814        }
815        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
816        if (source == null) {
817            Slog.w(TAG, "Local playback device not available");
818            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
819            return;
820        }
821        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
822                source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback);
823        if (action == null) {
824            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
825            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
826            return;
827        }
828        addAndStartAction(action);
829    }
830
831    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
832        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
833        try {
834            listener.asBinder().linkToDeath(record, 0);
835        } catch (RemoteException e) {
836            Slog.w(TAG, "Listener already died");
837            return;
838        }
839        synchronized (mLock) {
840            mHotplugEventListenerRecords.add(record);
841            mHotplugEventListeners.add(listener);
842        }
843    }
844
845    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
846        synchronized (mLock) {
847            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
848                if (record.mListener.asBinder() == listener.asBinder()) {
849                    listener.asBinder().unlinkToDeath(record, 0);
850                    mHotplugEventListenerRecords.remove(record);
851                    break;
852                }
853            }
854            mHotplugEventListeners.remove(listener);
855        }
856    }
857
858    private void invokeCallback(IHdmiControlCallback callback, int result) {
859        try {
860            callback.onComplete(result);
861        } catch (RemoteException e) {
862            Slog.e(TAG, "Invoking callback failed:" + e);
863        }
864    }
865
866    HdmiCecDeviceInfo getAvrDeviceInfo() {
867        return mCecController.getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
868    }
869
870    void setAudioStatus(boolean mute, int volume) {
871        // TODO: Hook up with AudioManager.
872    }
873
874    boolean isInPresetInstallationMode() {
875        synchronized (mLock) {
876            return !mInputChangeEnabled;
877        }
878    }
879
880    /**
881     * Called when a device is removed or removal of device is detected.
882     *
883     * @param address a logical address of a device to be removed
884     */
885    void removeCecDevice(int address) {
886        mCecController.removeDeviceInfo(address);
887        mCecMessageCache.flushMessagesFrom(address);
888    }
889
890    HdmiCecMessageCache getCecMessageCache() {
891        return mCecMessageCache;
892    }
893}
894