HdmiControlService.java revision 42c9800f4f3acef10d19dca39e8b739546407c04
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.Build;
28import android.os.Handler;
29import android.os.HandlerThread;
30import android.os.IBinder;
31import android.os.Looper;
32import android.os.RemoteException;
33import android.util.Slog;
34import android.util.SparseArray;
35import android.util.SparseIntArray;
36
37import com.android.internal.annotations.GuardedBy;
38import com.android.server.SystemService;
39import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback;
40import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback;
41
42import java.util.ArrayList;
43import java.util.Iterator;
44import java.util.LinkedList;
45import java.util.List;
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 version of CEC.
243     */
244    int getCecVersion() {
245        return mCecController.getVersion();
246    }
247
248    /**
249     * Returns a list of {@link HdmiCecDeviceInfo}.
250     *
251     * @param includeLocalDevice whether to include local devices
252     */
253    List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includeLocalDevice) {
254        assertRunOnServiceThread();
255        return mCecController.getDeviceInfoList(includeLocalDevice);
256    }
257
258    /**
259     * Add and start a new {@link FeatureAction} to the action queue.
260     *
261     * @param action {@link FeatureAction} to add and start
262     */
263    void addAndStartAction(final FeatureAction action) {
264        // TODO: may need to check the number of stale actions.
265        runOnServiceThread(new Runnable() {
266            @Override
267            public void run() {
268                mActions.add(action);
269                action.start();
270            }
271        });
272    }
273
274    void setSystemAudioMode(boolean on) {
275        synchronized (mLock) {
276            mSystemAudioMode = on;
277        }
278    }
279
280    boolean getSystemAudioMode() {
281        synchronized (mLock) {
282            return mSystemAudioMode;
283        }
284    }
285
286    // See if we have an action of a given type in progress.
287    <T extends FeatureAction> boolean hasAction(final Class<T> clazz) {
288        for (FeatureAction action : mActions) {
289            if (action.getClass().equals(clazz)) {
290                return true;
291            }
292        }
293        return false;
294    }
295
296    /**
297     * Remove the given {@link FeatureAction} object from the action queue.
298     *
299     * @param action {@link FeatureAction} to remove
300     */
301    void removeAction(final FeatureAction action) {
302        assertRunOnServiceThread();
303        mActions.remove(action);
304    }
305
306    // Remove all actions matched with the given Class type.
307    private <T extends FeatureAction> void removeAction(final Class<T> clazz) {
308        removeActionExcept(clazz, null);
309    }
310
311    // Remove all actions matched with the given Class type besides |exception|.
312    <T extends FeatureAction> void removeActionExcept(final Class<T> clazz,
313            final FeatureAction exception) {
314        assertRunOnServiceThread();
315        Iterator<FeatureAction> iter = mActions.iterator();
316        while (iter.hasNext()) {
317            FeatureAction action = iter.next();
318            if (action != exception && action.getClass().equals(clazz)) {
319                action.clear();
320                mActions.remove(action);
321            }
322        }
323    }
324
325    private void runOnServiceThread(Runnable runnable) {
326        mHandler.post(runnable);
327    }
328
329    void runOnServiceThreadAtFrontOfQueue(Runnable runnable) {
330        mHandler.postAtFrontOfQueue(runnable);
331    }
332
333    private void assertRunOnServiceThread() {
334        if (Looper.myLooper() != mHandler.getLooper()) {
335            throw new IllegalStateException("Should run on service thread.");
336        }
337    }
338
339    /**
340     * Change ARC status into the given {@code enabled} status.
341     *
342     * @return {@code true} if ARC was in "Enabled" status
343     */
344    boolean setArcStatus(boolean enabled) {
345        assertRunOnServiceThread();
346        synchronized (mLock) {
347            boolean oldStatus = mArcStatusEnabled;
348            // 1. Enable/disable ARC circuit.
349            mCecController.setAudioReturnChannel(enabled);
350
351            // TODO: notify arc mode change to AudioManager.
352
353            // 2. Update arc status;
354            mArcStatusEnabled = enabled;
355            return oldStatus;
356        }
357    }
358
359    /**
360     * Transmit a CEC command to CEC bus.
361     *
362     * @param command CEC command to send out
363     * @param callback interface used to the result of send command
364     */
365    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
366        mCecController.sendCommand(command, callback);
367    }
368
369    void sendCecCommand(HdmiCecMessage command) {
370        mCecController.sendCommand(command, null);
371    }
372
373    boolean handleCecCommand(HdmiCecMessage message) {
374        // Cache incoming message. Note that it caches only white-listed one.
375        mCecMessageCache.cacheMessage(message);
376
377        // Commands that queries system information replies directly instead
378        // of creating FeatureAction because they are state-less.
379        // TODO: move the leftover message to local device.
380        switch (message.getOpcode()) {
381            case HdmiCec.MESSAGE_INITIATE_ARC:
382                handleInitiateArc(message);
383                return true;
384            case HdmiCec.MESSAGE_TERMINATE_ARC:
385                handleTerminateArc(message);
386                return true;
387            case HdmiCec.MESSAGE_SET_SYSTEM_AUDIO_MODE:
388                handleSetSystemAudioMode(message);
389                return true;
390            case HdmiCec.MESSAGE_SYSTEM_AUDIO_MODE_STATUS:
391                handleSystemAudioModeStatus(message);
392                return true;
393            default:
394                if (dispatchMessageToAction(message)) {
395                    return true;
396                }
397                break;
398        }
399
400        return dispatchMessageToLocalDevice(message);
401    }
402
403    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
404        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
405            if (device.dispatchMessage(message)) {
406                return true;
407            }
408        }
409        return false;
410    }
411
412    /**
413     * Called when a new hotplug event is issued.
414     *
415     * @param portNo hdmi port number where hot plug event issued.
416     * @param connected whether to be plugged in or not
417     */
418    void onHotplug(int portNo, boolean connected) {
419        // TODO: Start "RequestArcInitiationAction" if ARC port.
420    }
421
422    /**
423     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
424     * devices.
425     *
426     * @param callback an interface used to get a list of all remote devices' address
427     * @param pickStrategy strategy how to pick polling candidates
428     * @param retryCount the number of retry used to send polling message to remote devices
429     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
430     */
431    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
432        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
433    }
434
435    private int checkPollStrategy(int pickStrategy) {
436        int strategy = pickStrategy & POLL_STRATEGY_MASK;
437        if (strategy == 0) {
438            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
439        }
440        int iterationStrategy = pickStrategy & POLL_ITERATION_STRATEGY_MASK;
441        if (iterationStrategy == 0) {
442            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
443        }
444        return strategy | iterationStrategy;
445    }
446
447    /**
448     * Launch device discovery sequence. It starts with clearing the existing device info list.
449     * Note that it assumes that logical address of all local devices is already allocated.
450     *
451     * @param sourceAddress a logical address of tv
452     */
453    void launchDeviceDiscovery(final int sourceAddress) {
454        // At first, clear all existing device infos.
455        mCecController.clearDeviceInfoList();
456        // TODO: flush cec message cache when CEC is turned off.
457
458        DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, sourceAddress,
459                new DeviceDiscoveryCallback() {
460                    @Override
461                    public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) {
462                        for (HdmiCecDeviceInfo info : deviceInfos) {
463                            addCecDevice(info);
464                        }
465
466                        // Add device info of all local devices.
467                        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
468                            addCecDevice(device.getDeviceInfo());
469                        }
470
471                        addAndStartAction(new HotplugDetectionAction(HdmiControlService.this,
472                                sourceAddress));
473                    }
474                });
475        addAndStartAction(action);
476    }
477
478    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
479        // TODO: find better name instead of model name.
480        String displayName = Build.MODEL;
481        return new HdmiCecDeviceInfo(logicalAddress,
482                getPhysicalAddress(), deviceType, getVendorId(), displayName);
483    }
484
485    private void handleInitiateArc(HdmiCecMessage message){
486        // In case where <Initiate Arc> is started by <Request ARC Initiation>
487        // need to clean up RequestArcInitiationAction.
488        removeAction(RequestArcInitiationAction.class);
489        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
490                message.getDestination(), message.getSource(), true);
491        addAndStartAction(action);
492    }
493
494    private void handleTerminateArc(HdmiCecMessage message) {
495        // In case where <Terminate Arc> is started by <Request ARC Termination>
496        // need to clean up RequestArcInitiationAction.
497        // TODO: check conditions of power status by calling is_connected api
498        // to be added soon.
499        removeAction(RequestArcTerminationAction.class);
500        SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
501                message.getDestination(), message.getSource(), false);
502        addAndStartAction(action);
503    }
504
505    private boolean dispatchMessageToAction(HdmiCecMessage message) {
506        for (FeatureAction action : mActions) {
507            if (action.processCommand(message)) {
508                return true;
509            }
510        }
511        Slog.w(TAG, "Unsupported cec command:" + message);
512        return false;
513    }
514
515    private void handleSetSystemAudioMode(HdmiCecMessage message) {
516        if (dispatchMessageToAction(message) || !isMessageForSystemAudio(message)) {
517            return;
518        }
519        SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this,
520                message.getDestination(), message.getSource(),
521                HdmiUtils.parseCommandParamSystemAudioStatus(message));
522        addAndStartAction(action);
523    }
524
525    private void handleSystemAudioModeStatus(HdmiCecMessage message) {
526        if (!isMessageForSystemAudio(message)) {
527            return;
528        }
529        setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message));
530    }
531
532    private boolean isMessageForSystemAudio(HdmiCecMessage message) {
533        if (message.getSource() != HdmiCec.ADDR_AUDIO_SYSTEM
534                || message.getDestination() != HdmiCec.ADDR_TV
535                || getAvrDeviceInfo() == null) {
536            Slog.w(TAG, "Skip abnormal CecMessage: " + message);
537            return false;
538        }
539        return true;
540    }
541
542    // Record class that monitors the event of the caller of being killed. Used to clean up
543    // the listener list and record list accordingly.
544    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
545        private final IHdmiHotplugEventListener mListener;
546
547        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
548            mListener = listener;
549        }
550
551        @Override
552        public void binderDied() {
553            synchronized (mLock) {
554                mHotplugEventListenerRecords.remove(this);
555                mHotplugEventListeners.remove(mListener);
556            }
557        }
558    }
559
560    void addCecDevice(HdmiCecDeviceInfo info) {
561        mCecController.addDeviceInfo(info);
562    }
563
564    private void enforceAccessPermission() {
565        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
566    }
567
568    private final class BinderService extends IHdmiControlService.Stub {
569        @Override
570        public int[] getSupportedTypes() {
571            enforceAccessPermission();
572            synchronized (mLock) {
573                return mLocalDevices;
574            }
575        }
576
577        @Override
578        public void oneTouchPlay(final IHdmiControlCallback callback) {
579            enforceAccessPermission();
580            runOnServiceThread(new Runnable() {
581                @Override
582                public void run() {
583                    HdmiControlService.this.oneTouchPlay(callback);
584                }
585            });
586        }
587
588        @Override
589        public void queryDisplayStatus(final IHdmiControlCallback callback) {
590            enforceAccessPermission();
591            runOnServiceThread(new Runnable() {
592                @Override
593                public void run() {
594                    HdmiControlService.this.queryDisplayStatus(callback);
595                }
596            });
597        }
598
599        @Override
600        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
601            enforceAccessPermission();
602            runOnServiceThread(new Runnable() {
603                @Override
604                public void run() {
605                    HdmiControlService.this.addHotplugEventListener(listener);
606                }
607            });
608        }
609
610        @Override
611        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
612            enforceAccessPermission();
613            runOnServiceThread(new Runnable() {
614                @Override
615                public void run() {
616                    HdmiControlService.this.removeHotplugEventListener(listener);
617                }
618            });
619        }
620    }
621
622    private void oneTouchPlay(IHdmiControlCallback callback) {
623        if (hasAction(OneTouchPlayAction.class)) {
624            Slog.w(TAG, "oneTouchPlay already in progress");
625            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
626            return;
627        }
628        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
629        if (source == null) {
630            Slog.w(TAG, "Local playback device not available");
631            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
632            return;
633        }
634        // TODO: Consider the case of multiple TV sets. For now we always direct the command
635        //       to the primary one.
636        OneTouchPlayAction action = OneTouchPlayAction.create(this,
637                source.getDeviceInfo().getLogicalAddress(),
638                source.getDeviceInfo().getPhysicalAddress(), HdmiCec.ADDR_TV, callback);
639        if (action == null) {
640            Slog.w(TAG, "Cannot initiate oneTouchPlay");
641            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
642            return;
643        }
644        addAndStartAction(action);
645    }
646
647    private void queryDisplayStatus(IHdmiControlCallback callback) {
648        if (hasAction(DevicePowerStatusAction.class)) {
649            Slog.w(TAG, "queryDisplayStatus already in progress");
650            invokeCallback(callback, HdmiCec.RESULT_ALREADY_IN_PROGRESS);
651            return;
652        }
653        HdmiCecLocalDevice source = mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
654        if (source == null) {
655            Slog.w(TAG, "Local playback device not available");
656            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
657            return;
658        }
659        DevicePowerStatusAction action = DevicePowerStatusAction.create(this,
660                source.getDeviceInfo().getLogicalAddress(), HdmiCec.ADDR_TV, callback);
661        if (action == null) {
662            Slog.w(TAG, "Cannot initiate queryDisplayStatus");
663            invokeCallback(callback, HdmiCec.RESULT_EXCEPTION);
664            return;
665        }
666        addAndStartAction(action);
667    }
668
669    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
670        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
671        try {
672            listener.asBinder().linkToDeath(record, 0);
673        } catch (RemoteException e) {
674            Slog.w(TAG, "Listener already died");
675            return;
676        }
677        synchronized (mLock) {
678            mHotplugEventListenerRecords.add(record);
679            mHotplugEventListeners.add(listener);
680        }
681    }
682
683    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
684        synchronized (mLock) {
685            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
686                if (record.mListener.asBinder() == listener.asBinder()) {
687                    listener.asBinder().unlinkToDeath(record, 0);
688                    mHotplugEventListenerRecords.remove(record);
689                    break;
690                }
691            }
692            mHotplugEventListeners.remove(listener);
693        }
694    }
695
696    private void invokeCallback(IHdmiControlCallback callback, int result) {
697        try {
698            callback.onComplete(result);
699        } catch (RemoteException e) {
700            Slog.e(TAG, "Invoking callback failed:" + e);
701        }
702    }
703
704    HdmiCecDeviceInfo getAvrDeviceInfo() {
705        return mCecController.getDeviceInfo(HdmiCec.ADDR_AUDIO_SYSTEM);
706    }
707
708    void setAudioStatus(boolean mute, int volume) {
709        // TODO: Hook up with AudioManager.
710    }
711
712    boolean isInPresetInstallationMode() {
713        // TODO: Implement this.
714        return false;
715    }
716
717    /**
718     * Called when a device is removed or removal of device is detected.
719     *
720     * @param address a logical address of a device to be removed
721     */
722    void removeCecDevice(int address) {
723        mCecController.removeDeviceInfo(address);
724        mCecMessageCache.flushMessagesFrom(address);
725    }
726
727    HdmiCecMessageCache getCecMessageCache() {
728        return mCecMessageCache;
729    }
730}
731