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