HdmiControlService.java revision 60cffce420db4c3395f86d3b9bb36003adf26f5d
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.IHdmiHotplugEventListener;
29import android.os.Build;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.IBinder;
33import android.os.Looper;
34import android.os.RemoteException;
35import android.util.Slog;
36import android.util.SparseArray;
37import android.util.SparseIntArray;
38
39import com.android.internal.annotations.GuardedBy;
40import com.android.server.SystemService;
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 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 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            if (on != mSystemAudioMode) {
450                mSystemAudioMode = on;
451                // TODO: Need to set the preference for SystemAudioMode.
452                // TODO: Need to handle the notification of changing the mode and
453                // to identify the notification should be handled in the service or TvSettings.
454            }
455        }
456    }
457
458    boolean getSystemAudioMode() {
459        synchronized (mLock) {
460            return mSystemAudioMode;
461        }
462    }
463
464    /**
465     * Whether a device of the specified physical address is connected to ARC enabled port.
466     */
467    boolean isConnectedToArcPort(int physicalAddress) {
468        for (HdmiPortInfo portInfo : mPortInfo) {
469            if (hasSameTopPort(portInfo.getAddress(), physicalAddress)
470                    && portInfo.isArcSupported()) {
471                return true;
472            }
473        }
474        return false;
475    }
476
477    // See if we have an action of a given type in progress.
478    <T extends FeatureAction> boolean hasAction(final Class<T> clazz) {
479        for (FeatureAction action : mActions) {
480            if (action.getClass().equals(clazz)) {
481                return true;
482            }
483        }
484        return false;
485    }
486
487    // Returns all actions matched with given class type.
488    <T extends FeatureAction> List<T> getActions(final Class<T> clazz) {
489        ArrayList<T> actions = new ArrayList<>();
490        for (FeatureAction action : mActions) {
491            if (action.getClass().equals(clazz)) {
492                actions.add((T) action);
493            }
494        }
495        return actions;
496    }
497
498    /**
499     * Remove the given {@link FeatureAction} object from the action queue.
500     *
501     * @param action {@link FeatureAction} to remove
502     */
503    void removeAction(final FeatureAction action) {
504        assertRunOnServiceThread();
505        mActions.remove(action);
506    }
507
508    // Remove all actions matched with the given Class type.
509    <T extends FeatureAction> void removeAction(final Class<T> clazz) {
510        removeActionExcept(clazz, null);
511    }
512
513    // Remove all actions matched with the given Class type besides |exception|.
514    <T extends FeatureAction> void removeActionExcept(final Class<T> clazz,
515            final FeatureAction exception) {
516        assertRunOnServiceThread();
517        Iterator<FeatureAction> iter = mActions.iterator();
518        while (iter.hasNext()) {
519            FeatureAction action = iter.next();
520            if (action != exception && action.getClass().equals(clazz)) {
521                action.clear();
522                mActions.remove(action);
523            }
524        }
525    }
526
527    private void runOnServiceThread(Runnable runnable) {
528        mHandler.post(runnable);
529    }
530
531    void runOnServiceThreadAtFrontOfQueue(Runnable runnable) {
532        mHandler.postAtFrontOfQueue(runnable);
533    }
534
535    private void assertRunOnServiceThread() {
536        if (Looper.myLooper() != mHandler.getLooper()) {
537            throw new IllegalStateException("Should run on service thread.");
538        }
539    }
540
541    /**
542     * Change ARC status into the given {@code enabled} status.
543     *
544     * @return {@code true} if ARC was in "Enabled" status
545     */
546    boolean setArcStatus(boolean enabled) {
547        assertRunOnServiceThread();
548        synchronized (mLock) {
549            boolean oldStatus = mArcStatusEnabled;
550            // 1. Enable/disable ARC circuit.
551            mCecController.setAudioReturnChannel(enabled);
552
553            // TODO: notify arc mode change to AudioManager.
554
555            // 2. Update arc status;
556            mArcStatusEnabled = enabled;
557            return oldStatus;
558        }
559    }
560
561    /**
562     * Returns whether ARC is enabled or not.
563     */
564    boolean getArcStatus() {
565        synchronized (mLock) {
566            return mArcStatusEnabled;
567        }
568    }
569
570    /**
571     * Transmit a CEC command to CEC bus.
572     *
573     * @param command CEC command to send out
574     * @param callback interface used to the result of send command
575     */
576    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
577        mCecController.sendCommand(command, callback);
578    }
579
580    void sendCecCommand(HdmiCecMessage command) {
581        mCecController.sendCommand(command, null);
582    }
583
584    boolean handleCecCommand(HdmiCecMessage message) {
585        // Cache incoming message. Note that it caches only white-listed one.
586        mCecMessageCache.cacheMessage(message);
587
588        // Commands that queries system information replies directly instead
589        // of creating FeatureAction because they are state-less.
590        // TODO: move the leftover message to local device.
591        switch (message.getOpcode()) {
592            case HdmiCec.MESSAGE_INITIATE_ARC:
593                handleInitiateArc(message);
594                return true;
595            case HdmiCec.MESSAGE_TERMINATE_ARC:
596                handleTerminateArc(message);
597                return true;
598            case HdmiCec.MESSAGE_SET_SYSTEM_AUDIO_MODE:
599                handleSetSystemAudioMode(message);
600                return true;
601            case HdmiCec.MESSAGE_SYSTEM_AUDIO_MODE_STATUS:
602                handleSystemAudioModeStatus(message);
603                return true;
604            default:
605                if (dispatchMessageToAction(message)) {
606                    return true;
607                }
608                break;
609        }
610
611        return dispatchMessageToLocalDevice(message);
612    }
613
614    private boolean dispatchMessageToAction(HdmiCecMessage message) {
615        for (FeatureAction action : mActions) {
616            if (action.processCommand(message)) {
617                return true;
618            }
619        }
620        return false;
621    }
622
623    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
624        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
625            if (device.dispatchMessage(message)) {
626                return true;
627            }
628        }
629
630        Slog.w(TAG, "Unhandled cec command:" + message);
631        return false;
632    }
633
634    /**
635     * Called when a new hotplug event is issued.
636     *
637     * @param portNo hdmi port number where hot plug event issued.
638     * @param connected whether to be plugged in or not
639     */
640    void onHotplug(int portNo, boolean connected) {
641        assertRunOnServiceThread();
642        // TODO: delegate onHotplug event to each local device.
643
644        // Tv device will have permanent HotplugDetectionAction.
645        List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class);
646        if (!hotplugActions.isEmpty()) {
647            // Note that hotplug action is single action running on a machine.
648            // "pollAllDevicesNow" cleans up timer and start poll action immediately.
649            hotplugActions.get(0).pollAllDevicesNow();
650        }
651
652        announceHotplugEvent(portNo, connected);
653    }
654
655    /**
656     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
657     * devices.
658     *
659     * @param callback an interface used to get a list of all remote devices' address
660     * @param pickStrategy strategy how to pick polling candidates
661     * @param retryCount the number of retry used to send polling message to remote devices
662     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
663     */
664    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
665        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
666    }
667
668    private int checkPollStrategy(int pickStrategy) {
669        int strategy = pickStrategy & POLL_STRATEGY_MASK;
670        if (strategy == 0) {
671            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
672        }
673        int iterationStrategy = pickStrategy & POLL_ITERATION_STRATEGY_MASK;
674        if (iterationStrategy == 0) {
675            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
676        }
677        return strategy | iterationStrategy;
678    }
679
680    void clearAllDeviceInfo() {
681        assertRunOnServiceThread();
682        mCecController.clearDeviceInfoList();
683    }
684
685    List<HdmiCecLocalDevice> getAllLocalDevices() {
686        assertRunOnServiceThread();
687        return mCecController.getLocalDeviceList();
688    }
689
690    /**
691     * Whether a device of the specified physical address and logical address exists
692     * in a device info list. However, both are minimal condition and it could
693     * be different device from the original one.
694     *
695     * @param physicalAddress physical address of a device to be searched
696     * @param logicalAddress logical address of a device to be searched
697     * @return true if exist; otherwise false
698     */
699    boolean isInDeviceList(int physicalAddress, int logicalAddress) {
700        assertRunOnServiceThread();
701        HdmiCecDeviceInfo device = mCecController.getDeviceInfo(logicalAddress);
702        if (device == null) {
703            return false;
704        }
705        return device.getPhysicalAddress() == physicalAddress;
706    }
707
708    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
709        // TODO: find better name instead of model name.
710        String displayName = Build.MODEL;
711        return new HdmiCecDeviceInfo(logicalAddress,
712                getPhysicalAddress(), deviceType, getVendorId(), displayName);
713    }
714
715    private void handleInitiateArc(HdmiCecMessage message){
716        // In case where <Initiate Arc> is started by <Request ARC Initiation>
717        // need to clean up RequestArcInitiationAction.
718        removeAction(RequestArcInitiationAction.class);
719        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
720                message.getDestination(), message.getSource(), true);
721        addAndStartAction(action);
722    }
723
724    private void handleTerminateArc(HdmiCecMessage message) {
725        // In case where <Terminate Arc> is started by <Request ARC Termination>
726        // need to clean up RequestArcInitiationAction.
727        // TODO: check conditions of power status by calling is_connected api
728        // to be added soon.
729        removeAction(RequestArcTerminationAction.class);
730        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
731                message.getDestination(), message.getSource(), false);
732        addAndStartAction(action);
733    }
734
735    private void handleSetSystemAudioMode(HdmiCecMessage message) {
736        if (dispatchMessageToAction(message) || !isMessageForSystemAudio(message)) {
737            return;
738        }
739        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
740                message.getDestination(), message.getSource(),
741                HdmiUtils.parseCommandParamSystemAudioStatus(message));
742        addAndStartAction(action);
743    }
744
745    private void handleSystemAudioModeStatus(HdmiCecMessage message) {
746        if (!isMessageForSystemAudio(message)) {
747            return;
748        }
749        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
750    }
751
752    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
753        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
754                || message.getDestination() != HdmiCec.ADDR_TV
755                || getAvrDeviceInfo() == null) {
756            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
757            return false;
758        }
759        return true;
760    }
761
762    // Record class that monitors the event of the caller of being killed. Used to clean up
763    // the listener list and record list accordingly.
764    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
765        private final IHdmiHotplugEventListener mListener;
766
767        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
768            mListener = listener;
769        }
770
771        @Override
772        public void binderDied() {
773            synchronized (mLock) {
774                mHotplugEventListenerRecords.remove(this);
775                mHotplugEventListeners.remove(mListener);
776            }
777        }
778    }
779
780    private void enforceAccessPermission() {
781        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
782    }
783
784    private final class BinderService extends IHdmiControlService.Stub {
785        @Override
786        public int[] getSupportedTypes() {
787            enforceAccessPermission();
788            // mLocalDevices is an unmodifiable list - no lock necesary.
789            int[] localDevices = new int[mLocalDevices.size()];
790            for (int i = 0; i < localDevices.length; ++i) {
791                localDevices[i] = mLocalDevices.get(i);
792            }
793            return localDevices;
794        }
795
796        @Override
797        public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) {
798            enforceAccessPermission();
799            runOnServiceThread(new Runnable() {
800                @Override
801                public void run() {
802                    HdmiCecLocalDeviceTv tv =
803                            (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV);
804                    if (tv == null) {
805                        Slog.w(TAG, "Local playback device not available");
806                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
807                        return;
808                    }
809                    tv.deviceSelect(logicalAddress, callback);
810                }
811            });
812        }
813
814        @Override
815        public void oneTouchPlay(final IHdmiControlCallback callback) {
816            enforceAccessPermission();
817            runOnServiceThread(new Runnable() {
818                @Override
819                public void run() {
820                    HdmiControlService.this.oneTouchPlay(callback);
821                }
822            });
823        }
824
825        @Override
826        public void queryDisplayStatus(final IHdmiControlCallback callback) {
827            enforceAccessPermission();
828            runOnServiceThread(new Runnable() {
829                @Override
830                public void run() {
831                    HdmiControlService.this.queryDisplayStatus(callback);
832                }
833            });
834        }
835
836        @Override
837        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
838            enforceAccessPermission();
839            runOnServiceThread(new Runnable() {
840                @Override
841                public void run() {
842                    HdmiControlService.this.addHotplugEventListener(listener);
843                }
844            });
845        }
846
847        @Override
848        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
849            enforceAccessPermission();
850            runOnServiceThread(new Runnable() {
851                @Override
852                public void run() {
853                    HdmiControlService.this.removeHotplugEventListener(listener);
854                }
855            });
856        }
857    }
858
859    private void oneTouchPlay(IHdmiControlCallback callback) {
860        if (hasAction(OneTouchPlayAction.class)) {
861            Slog.w(TAG, "oneTouchPlay already in progress");
862            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
863            return;
864        }
865        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
866        if (source == null) {
867            Slog.w(TAG, "Local playback device not available");
868            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
869            return;
870        }
871        // TODO: Consider the case of multiple TV sets. For now we always direct the command
872        //       to the primary one.
873        OneTouchPlayAction action = OneTouchPlayAction.create(this,
874                source.getDeviceInfo().getLogicalAddress(),
875                source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback);
876        if (action == null) {
877            Slog.w(TAG, "Cannot initiate oneTouchPlay");
878            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
879            return;
880        }
881        addAndStartAction(action);
882    }
883
884    private void queryDisplayStatus(IHdmiControlCallback callback) {
885        if (hasAction(DevicePowerStatusAction.class)) {
886            Slog.w(TAG, "queryDisplayStatus already in progress");
887            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
888            return;
889        }
890        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
891        if (source == null) {
892            Slog.w(TAG, "Local playback device not available");
893            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
894            return;
895        }
896        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
897                source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback);
898        if (action == null) {
899            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
900            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
901            return;
902        }
903        addAndStartAction(action);
904    }
905
906    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
907        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
908        try {
909            listener.asBinder().linkToDeath(record, 0);
910        } catch (RemoteException e) {
911            Slog.w(TAG, "Listener already died");
912            return;
913        }
914        synchronized (mLock) {
915            mHotplugEventListenerRecords.add(record);
916            mHotplugEventListeners.add(listener);
917        }
918    }
919
920    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
921        synchronized (mLock) {
922            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
923                if (record.mListener.asBinder() == listener.asBinder()) {
924                    listener.asBinder().unlinkToDeath(record, 0);
925                    mHotplugEventListenerRecords.remove(record);
926                    break;
927                }
928            }
929            mHotplugEventListeners.remove(listener);
930        }
931    }
932
933    private void invokeCallback(IHdmiControlCallback callback, int result) {
934        try {
935            callback.onComplete(result);
936        } catch (RemoteException e) {
937            Slog.e(TAG, "Invoking callback failed:" + e);
938        }
939    }
940
941    HdmiCecDeviceInfo getAvrDeviceInfo() {
942        return mCecController.getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
943    }
944
945    void setAudioStatus(boolean mute, int volume) {
946        // TODO: Hook up with AudioManager.
947    }
948
949    boolean isInPresetInstallationMode() {
950        synchronized (mLock) {
951            return !mInputChangeEnabled;
952        }
953    }
954
955    /**
956     * Called when a device is newly added or a new device is detected.
957     *
958     * @param info device info of a new device.
959     */
960    void addCecDevice(HdmiCecDeviceInfo info) {
961        mCecController.addDeviceInfo(info);
962
963        // TODO: announce new device detection.
964    }
965
966    /**
967     * Called when a device is removed or removal of device is detected.
968     *
969     * @param address a logical address of a device to be removed
970     */
971    void removeCecDevice(int address) {
972        mCecController.removeDeviceInfo(address);
973        mCecMessageCache.flushMessagesFrom(address);
974
975        // TODO: announce a device removal.
976    }
977
978    private void announceHotplugEvent(int portNo, boolean connected) {
979        HdmiHotplugEvent event = new HdmiHotplugEvent(portNo, connected);
980        synchronized (mLock) {
981            for (IHdmiHotplugEventListener listener : mHotplugEventListeners) {
982                invokeHotplugEventListener(listener, event);
983            }
984        }
985    }
986
987    private void invokeHotplugEventListener(IHdmiHotplugEventListener listener,
988            HdmiHotplugEvent event) {
989        try {
990            listener.onReceived(event);
991        } catch (RemoteException e) {
992            Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e);
993        }
994    }
995
996    HdmiCecMessageCache getCecMessageCache() {
997        return mCecMessageCache;
998    }
999
1000    private static boolean hasSameTopPort(int path1, int path2) {
1001        return (path1 & HdmiConstants.ROUTING_PATH_TOP_MASK)
1002                == (path2 & HdmiConstants.ROUTING_PATH_TOP_MASK);
1003    }
1004
1005    /**
1006     * Whether the given path is located in the tail of current active path.
1007     *
1008     * @param path to be tested
1009     * @return true if the given path is located in the tail of current active path; otherwise,
1010     *         false
1011     */
1012    // TODO: move this to local device tv.
1013    boolean isTailOfActivePath(int path) {
1014        // If active routing path is internal source, return false.
1015        if (mActiveRoutingPath == 0) {
1016            return false;
1017        }
1018        for (int i = 12; i >= 0; i -= 4) {
1019            int curActivePath = (mActiveRoutingPath >> i) & 0xF;
1020            if (curActivePath == 0) {
1021                return true;
1022            } else {
1023                int curPath = (path >> i) & 0xF;
1024                if (curPath != curActivePath) {
1025                    return false;
1026                }
1027            }
1028        }
1029        return false;
1030    }
1031}
1032