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