HdmiControlService.java revision e81e108c4035ea8933525baa8108cb392f8abf5d
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    /**
364     * Add a new {@link HdmiCecDeviceInfo} to controller.
365     *
366     * @param deviceInfo new device information object to add
367     */
368    void addDeviceInfo(HdmiCecDeviceInfo deviceInfo) {
369        // TODO: Implement this.
370    }
371
372    boolean handleCecCommand(HdmiCecMessage message) {
373        // Cache incoming message. Note that it caches only white-listed one.
374        mCecMessageCache.cacheMessage(message);
375
376        // Commands that queries system information replies directly instead
377        // of creating FeatureAction because they are state-less.
378        switch (message.getOpcode()) {
379            case HdmiCec.MESSAGE_GET_MENU_LANGUAGE:
380                handleGetMenuLanguage(message);
381                return true;
382            case HdmiCec.MESSAGE_GIVE_OSD_NAME:
383                handleGiveOsdName(message);
384                return true;
385            case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS:
386                handleGivePhysicalAddress(message);
387                return true;
388            case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID:
389                handleGiveDeviceVendorId(message);
390                return true;
391            case HdmiCec.MESSAGE_GET_CEC_VERSION:
392                handleGetCecVersion(message);
393                return true;
394            case HdmiCec.MESSAGE_INITIATE_ARC:
395                handleInitiateArc(message);
396                return true;
397            case HdmiCec.MESSAGE_TERMINATE_ARC:
398                handleTerminateArc(message);
399                return true;
400            case HdmiCec.MESSAGE_REPORT_PHYSICAL_ADDRESS:
401                handleReportPhysicalAddress(message);
402                return true;
403            case HdmiCec.MESSAGE_SET_SYSTEM_AUDIO_MODE:
404                handleSetSystemAudioMode(message);
405                return true;
406            case HdmiCec.MESSAGE_SYSTEM_AUDIO_MODE_STATUS:
407                handleSystemAudioModeStatus(message);
408                return true;
409            default:
410                return dispatchMessageToAction(message);
411        }
412    }
413
414    /**
415     * Called when a new hotplug event is issued.
416     *
417     * @param portNo hdmi port number where hot plug event issued.
418     * @param connected whether to be plugged in or not
419     */
420    void onHotplug(int portNo, boolean connected) {
421        // TODO: Start "RequestArcInitiationAction" if ARC port.
422    }
423
424    /**
425     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
426     * devices.
427     *
428     * @param callback an interface used to get a list of all remote devices' address
429     * @param pickStrategy strategy how to pick polling candidates
430     * @param retryCount the number of retry used to send polling message to remote devices
431     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
432     */
433    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
434        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
435    }
436
437    private int checkPollStrategy(int pickStrategy) {
438        int strategy = pickStrategy & POLL_STRATEGY_MASK;
439        if (strategy == 0) {
440            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
441        }
442        int iterationStrategy = pickStrategy & POLL_ITERATION_STRATEGY_MASK;
443        if (iterationStrategy == 0) {
444            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
445        }
446        return strategy | iterationStrategy;
447    }
448
449    /**
450     * Launch device discovery sequence. It starts with clearing the existing device info list.
451     * Note that it assumes that logical address of all local devices is already allocated.
452     *
453     * @param sourceAddress a logical address of tv
454     */
455    void launchDeviceDiscovery(final int sourceAddress) {
456        // At first, clear all existing device infos.
457        mCecController.clearDeviceInfoList();
458        mCecMessageCache.flushAll();
459
460        // TODO: check whether TV is one of local devices.
461        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, sourceAddress,
462                new DeviceDiscoveryCallback() {
463                    @Override
464                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
465                        for (HdmiCecDeviceInfo info : deviceInfos) {
466                            mCecController.addDeviceInfo(info);
467                        }
468
469                        // Add device info of all local devices.
470                        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
471                            mCecController.addDeviceInfo(device.getDeviceInfo());
472                        }
473
474                        addAndStartAction(new HotplugDetectionAction(HdmiControlService.this,
475                                sourceAddress));
476                    }
477                });
478        addAndStartAction(action);
479    }
480
481    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
482        // TODO: get device name read from system configuration.
483        String displayName = HdmiCec.getDefaultDeviceName(logicalAddress);
484        return new HdmiCecDeviceInfo(logicalAddress,
485                getPhysicalAddress(), deviceType, getVendorId(), displayName);
486    }
487
488    private void handleReportPhysicalAddress(HdmiCecMessage message) {
489        // At first, try to consume it.
490        if (dispatchMessageToAction(message)) {
491            return;
492        }
493
494        // Ignore if [Device Discovery Action] is going on.
495        if (hasAction(DeviceDiscoveryAction.class)) {
496            Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> "
497                    + "because Device Discovery Action is on-going:" + message);
498            return;
499        }
500
501        // TODO: start new device action.
502    }
503
504    private void handleInitiateArc(HdmiCecMessage message){
505        // In case where <Initiate Arc> is started by <Request ARC Initiation>
506        // need to clean up RequestArcInitiationAction.
507        removeAction(RequestArcInitiationAction.class);
508        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
509                message.getDestination(), message.getSource(), true);
510        addAndStartAction(action);
511    }
512
513    private void handleTerminateArc(HdmiCecMessage message) {
514        // In case where <Terminate Arc> is started by <Request ARC Termination>
515        // need to clean up RequestArcInitiationAction.
516        // TODO: check conditions of power status by calling is_connected api
517        // to be added soon.
518        removeAction(RequestArcTerminationAction.class);
519        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
520                message.getDestination(), message.getSource(), false);
521        addAndStartAction(action);
522    }
523
524    private void handleGetCecVersion(HdmiCecMessage message) {
525        int version = mCecController.getVersion();
526        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(),
527                message.getSource(),
528                version);
529        sendCecCommand(cecMessage);
530    }
531
532    private void handleGiveDeviceVendorId(HdmiCecMessage message) {
533        int vendorId = mCecController.getVendorId();
534        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand(
535                message.getDestination(), vendorId);
536        sendCecCommand(cecMessage);
537    }
538
539    private void handleGivePhysicalAddress(HdmiCecMessage message) {
540        int physicalAddress = mCecController.getPhysicalAddress();
541        int deviceType = HdmiCec.getTypeFromAddress(message.getDestination());
542        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
543                message.getDestination(), physicalAddress, deviceType);
544        sendCecCommand(cecMessage);
545    }
546
547    private void handleGiveOsdName(HdmiCecMessage message) {
548        // TODO: read device name from settings or property.
549        String name = HdmiCec.getDefaultDeviceName(message.getDestination());
550        HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand(
551                message.getDestination(), message.getSource(), name);
552        if (cecMessage != null) {
553            sendCecCommand(cecMessage);
554        } else {
555            Slog.w(TAG, "Failed to build <Get Osd Name>:" + name);
556        }
557    }
558
559    private void handleGetMenuLanguage(HdmiCecMessage message) {
560        // Only 0 (TV), 14 (specific use) can answer.
561        if (message.getDestination() != HdmiCec.ADDR_TV
562                && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) {
563            Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString());
564            sendCecCommand(
565                    HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(),
566                            message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE,
567                            HdmiConstants.ABORT_UNRECOGNIZED_MODE));
568            return;
569        }
570
571        HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand(
572                message.getDestination(),
573                Locale.getDefault().getISO3Language());
574        // TODO: figure out how to handle failed to get language code.
575        if (command != null) {
576            sendCecCommand(command);
577        } else {
578            Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString());
579        }
580    }
581
582    private boolean dispatchMessageToAction(HdmiCecMessage message) {
583        for (FeatureAction action : mActions) {
584            if (action.processCommand(message)) {
585                return true;
586            }
587        }
588        Slog.w(TAG, "Unsupported cec command:" + message);
589        return false;
590    }
591
592    private void handleSetSystemAudioMode(HdmiCecMessage message) {
593        if (dispatchMessageToAction(message) || !isMessageForSystemAudio(message)) {
594            return;
595        }
596        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
597                message.getDestination(), message.getSource(),
598                HdmiUtils.parseCommandParamSystemAudioStatus(message));
599        addAndStartAction(action);
600    }
601
602    private void handleSystemAudioModeStatus(HdmiCecMessage message) {
603        if (!isMessageForSystemAudio(message)) {
604            return;
605        }
606        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
607    }
608
609    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
610        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
611                || message.getDestination() != HdmiCec.ADDR_TV
612                || getAvrDeviceInfo() == null) {
613            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
614            return false;
615        }
616        return true;
617    }
618
619    // Record class that monitors the event of the caller of being killed. Used to clean up
620    // the listener list and record list accordingly.
621    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
622        private final IHdmiHotplugEventListener mListener;
623
624        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
625            mListener = listener;
626        }
627
628        @Override
629        public void binderDied() {
630            synchronized (mLock) {
631                mHotplugEventListenerRecords.remove(this);
632                mHotplugEventListeners.remove(mListener);
633            }
634        }
635    }
636
637    void addCecDevice(HdmiCecDeviceInfo info) {
638        mCecController.addDeviceInfo(info);
639    }
640
641    private void enforceAccessPermission() {
642        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
643    }
644
645    private final class BinderService extends IHdmiControlService.Stub {
646        @Override
647        public int[] getSupportedTypes() {
648            enforceAccessPermission();
649            synchronized (mLock) {
650                return mLocalDevices;
651            }
652        }
653
654        @Override
655        public void oneTouchPlay(final IHdmiControlCallback callback) {
656            enforceAccessPermission();
657            runOnServiceThread(new Runnable() {
658                @Override
659                public void run() {
660                    HdmiControlService.this.oneTouchPlay(callback);
661                }
662            });
663        }
664
665        @Override
666        public void queryDisplayStatus(final IHdmiControlCallback callback) {
667            enforceAccessPermission();
668            runOnServiceThread(new Runnable() {
669                @Override
670                public void run() {
671                    HdmiControlService.this.queryDisplayStatus(callback);
672                }
673            });
674        }
675
676        @Override
677        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
678            enforceAccessPermission();
679            runOnServiceThread(new Runnable() {
680                @Override
681                public void run() {
682                    HdmiControlService.this.addHotplugEventListener(listener);
683                }
684            });
685        }
686
687        @Override
688        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
689            enforceAccessPermission();
690            runOnServiceThread(new Runnable() {
691                @Override
692                public void run() {
693                    HdmiControlService.this.removeHotplugEventListener(listener);
694                }
695            });
696        }
697    }
698
699    private void oneTouchPlay(IHdmiControlCallback callback) {
700        if (hasAction(OneTouchPlayAction.class)) {
701            Slog.w(TAG, "oneTouchPlay already in progress");
702            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
703            return;
704        }
705        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
706        if (source == null) {
707            Slog.w(TAG, "Local playback device not available");
708            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
709            return;
710        }
711        // TODO: Consider the case of multiple TV sets. For now we always direct the command
712        //       to the primary one.
713        OneTouchPlayAction action = OneTouchPlayAction.create(this,
714                source.getDeviceInfo().getLogicalAddress(),
715                source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback);
716        if (action == null) {
717            Slog.w(TAG, "Cannot initiate oneTouchPlay");
718            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
719            return;
720        }
721        addAndStartAction(action);
722    }
723
724    private void queryDisplayStatus(IHdmiControlCallback callback) {
725        if (hasAction(DevicePowerStatusAction.class)) {
726            Slog.w(TAG, "queryDisplayStatus already in progress");
727            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
728            return;
729        }
730        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
731        if (source == null) {
732            Slog.w(TAG, "Local playback device not available");
733            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
734            return;
735        }
736        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
737                source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback);
738        if (action == null) {
739            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
740            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
741            return;
742        }
743        addAndStartAction(action);
744    }
745
746    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
747        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
748        try {
749            listener.asBinder().linkToDeath(record, 0);
750        } catch (RemoteException e) {
751            Slog.w(TAG, "Listener already died");
752            return;
753        }
754        synchronized (mLock) {
755            mHotplugEventListenerRecords.add(record);
756            mHotplugEventListeners.add(listener);
757        }
758    }
759
760    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
761        synchronized (mLock) {
762            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
763                if (record.mListener.asBinder() == listener.asBinder()) {
764                    listener.asBinder().unlinkToDeath(record, 0);
765                    mHotplugEventListenerRecords.remove(record);
766                    break;
767                }
768            }
769            mHotplugEventListeners.remove(listener);
770        }
771    }
772
773    private void invokeCallback(IHdmiControlCallback callback, int result) {
774        try {
775            callback.onComplete(result);
776        } catch (RemoteException e) {
777            Slog.e(TAG, "Invoking callback failed:" + e);
778        }
779    }
780
781    HdmiCecDeviceInfo getAvrDeviceInfo() {
782        return mCecController.getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
783    }
784
785    void setAudioStatus(boolean mute, int volume) {
786        // TODO: Hook up with AudioManager.
787    }
788
789    boolean isInPresetInstallationMode() {
790        // TODO: Implement this.
791        return false;
792    }
793
794    /**
795     * Called when a device is removed or removal of device is detected.
796     *
797     * @param address a logical address of a device to be removed
798     */
799    void removeCecDevice(int address) {
800        mCecController.removeDeviceInfo(address);
801        mCecMessageCache.flushMessagesFrom(address);
802    }
803
804    HdmiCecMessageCache getCecMessageCache() {
805        return mCecMessageCache;
806    }
807}
808