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