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