HdmiControlService.java revision a5b7414970c85217e88015e78ecbc5ba093dead3
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.hdmi;
18
19import android.annotation.Nullable;
20import android.content.Context;
21import android.hardware.hdmi.HdmiCec;
22import android.hardware.hdmi.HdmiCecDeviceInfo;
23import android.hardware.hdmi.HdmiCecMessage;
24import android.hardware.hdmi.HdmiHotplugEvent;
25import android.hardware.hdmi.HdmiPortInfo;
26import android.hardware.hdmi.IHdmiControlCallback;
27import android.hardware.hdmi.IHdmiControlService;
28import android.hardware.hdmi.IHdmiDeviceEventListener;
29import android.hardware.hdmi.IHdmiHotplugEventListener;
30import android.hardware.hdmi.IHdmiSystemAudioModeChangeListener;
31import android.os.Build;
32import android.os.Handler;
33import android.os.HandlerThread;
34import android.os.IBinder;
35import android.os.Looper;
36import android.os.RemoteException;
37import android.util.Slog;
38import android.util.SparseArray;
39import android.util.SparseIntArray;
40
41import com.android.internal.annotations.GuardedBy;
42import com.android.server.SystemService;
43import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly;
44import com.android.server.hdmi.HdmiCecController.AllocateAddressCallback;
45
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.List;
49
50/**
51 * Provides a service for sending and processing HDMI control messages,
52 * HDMI-CEC and MHL control command, and providing the information on both standard.
53 */
54public final class HdmiControlService extends SystemService {
55    private static final String TAG = "HdmiControlService";
56
57    // TODO: Rename the permission to HDMI_CONTROL.
58    private static final String PERMISSION = "android.permission.HDMI_CEC";
59
60    /**
61     * Interface to report send result.
62     */
63    interface SendMessageCallback {
64        /**
65         * Called when {@link HdmiControlService#sendCecCommand} is completed.
66         *
67         * @param error result of send request.
68         * @see {@link #SEND_RESULT_SUCCESS}
69         * @see {@link #SEND_RESULT_NAK}
70         * @see {@link #SEND_RESULT_FAILURE}
71         */
72        void onSendCompleted(int error);
73    }
74
75    /**
76     * Interface to get a list of available logical devices.
77     */
78    interface DevicePollingCallback {
79        /**
80         * Called when device polling is finished.
81         *
82         * @param ackedAddress a list of logical addresses of available devices
83         */
84        void onPollingFinished(List<Integer> ackedAddress);
85    }
86
87    // A thread to handle synchronous IO of CEC and MHL control service.
88    // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms)
89    // and sparse call it shares a thread to handle IO operations.
90    private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread");
91
92    // Used to synchronize the access to the service.
93    private final Object mLock = new Object();
94
95    // Type of logical devices hosted in the system. Stored in the unmodifiable list.
96    private final List<Integer> mLocalDevices;
97
98    // List of listeners registered by callers that want to get notified of
99    // hotplug events.
100    @GuardedBy("mLock")
101    private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>();
102
103    // List of records for hotplug event listener to handle the the caller killed in action.
104    @GuardedBy("mLock")
105    private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
106            new ArrayList<>();
107
108    // List of listeners registered by callers that want to get notified of
109    // device status events.
110    @GuardedBy("mLock")
111    private final ArrayList<IHdmiDeviceEventListener> mDeviceEventListeners = new ArrayList<>();
112
113    // List of records for device event listener to handle the the caller killed in action.
114    @GuardedBy("mLock")
115    private final ArrayList<DeviceEventListenerRecord> mDeviceEventListenerRecords =
116            new ArrayList<>();
117
118    // List of listeners registered by callers that want to get notified of
119    // system audio mode changes.
120    private final ArrayList<IHdmiSystemAudioModeChangeListener>
121            mSystemAudioModeChangeListeners = new ArrayList<>();
122    // List of records for system audio mode change to handle the the caller killed in action.
123    private final ArrayList<SystemAudioModeChangeListenerRecord>
124            mSystemAudioModeChangeListenerRecords = new ArrayList<>();
125
126    // Handler used to run a task in service thread.
127    private final Handler mHandler = new Handler();
128
129    @Nullable
130    private HdmiCecController mCecController;
131
132    @Nullable
133    private HdmiMhlController mMhlController;
134
135    // HDMI port information. Stored in the unmodifiable list to keep the static information
136    // from being modified.
137    private List<HdmiPortInfo> mPortInfo;
138
139    public HdmiControlService(Context context) {
140        super(context);
141        mLocalDevices = HdmiUtils.asImmutableList(getContext().getResources().getIntArray(
142                com.android.internal.R.array.config_hdmiCecLogicalDeviceType));
143    }
144
145    @Override
146    public void onStart() {
147        mIoThread.start();
148        mCecController = HdmiCecController.create(this);
149
150        if (mCecController != null) {
151            initializeLocalDevices(mLocalDevices);
152        } else {
153            Slog.i(TAG, "Device does not support HDMI-CEC.");
154        }
155
156        mMhlController = HdmiMhlController.create(this);
157        if (mMhlController == null) {
158            Slog.i(TAG, "Device does not support MHL-control.");
159        }
160        mPortInfo = initPortInfo();
161        publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService());
162
163        // TODO: Read the preference for SystemAudioMode and initialize mSystemAudioMode and
164        // start to monitor the preference value and invoke SystemAudioActionFromTv if needed.
165    }
166
167    @ServiceThreadOnly
168    private void initializeLocalDevices(final List<Integer> deviceTypes) {
169        assertRunOnServiceThread();
170        // A container for [Logical Address, Local device info].
171        final SparseArray<HdmiCecLocalDevice> devices = new SparseArray<>();
172        final SparseIntArray finished = new SparseIntArray();
173        mCecController.clearLogicalAddress();
174        for (int type : deviceTypes) {
175            final HdmiCecLocalDevice localDevice = HdmiCecLocalDevice.create(this, type);
176            localDevice.init();
177            mCecController.allocateLogicalAddress(type,
178                    localDevice.getPreferredAddress(), new AllocateAddressCallback() {
179                @Override
180                public void onAllocated(int deviceType, int logicalAddress) {
181                    if (logicalAddress == HdmiCec.ADDR_UNREGISTERED) {
182                        Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType + "]");
183                    } else {
184                        HdmiCecDeviceInfo deviceInfo = createDeviceInfo(logicalAddress, deviceType);
185                        localDevice.setDeviceInfo(deviceInfo);
186                        mCecController.addLocalDevice(deviceType, localDevice);
187                        mCecController.addLogicalAddress(logicalAddress);
188                        devices.append(logicalAddress, localDevice);
189                    }
190                    finished.append(deviceType, logicalAddress);
191
192                    // Address allocation completed for all devices. Notify each device.
193                    if (deviceTypes.size() == finished.size()) {
194                        notifyAddressAllocated(devices);
195                    }
196                }
197            });
198        }
199    }
200
201    @ServiceThreadOnly
202    private void notifyAddressAllocated(SparseArray<HdmiCecLocalDevice> devices) {
203        assertRunOnServiceThread();
204        for (int i = 0; i < devices.size(); ++i) {
205            int address = devices.keyAt(i);
206            HdmiCecLocalDevice device = devices.valueAt(i);
207            device.handleAddressAllocated(address);
208        }
209    }
210
211    // Initialize HDMI port information. Combine the information from CEC and MHL HAL and
212    // keep them in one place.
213    @ServiceThreadOnly
214    private List<HdmiPortInfo> initPortInfo() {
215        assertRunOnServiceThread();
216        HdmiPortInfo[] cecPortInfo = null;
217
218        // CEC HAL provides majority of the info while MHL does only MHL support flag for
219        // each port. Return empty array if CEC HAL didn't provide the info.
220        if (mCecController != null) {
221            cecPortInfo = mCecController.getPortInfos();
222        }
223        if (cecPortInfo == null) {
224            return Collections.emptyList();
225        }
226
227        HdmiPortInfo[] mhlPortInfo = new HdmiPortInfo[0];
228        if (mMhlController != null) {
229            // TODO: Implement plumbing logic to get MHL port information.
230            // mhlPortInfo = mMhlController.getPortInfos();
231        }
232
233        // Use the id (port number) to find the matched info between CEC and MHL to combine them
234        // into one. Leave the field `mhlSupported` to false if matched MHL entry is not found.
235        ArrayList<HdmiPortInfo> result = new ArrayList<>(cecPortInfo.length);
236        for (int i = 0; i < cecPortInfo.length; ++i) {
237            HdmiPortInfo cec = cecPortInfo[i];
238            int id = cec.getId();
239            boolean mhlInfoFound = false;
240            for (HdmiPortInfo mhl : mhlPortInfo) {
241                if (id == mhl.getId()) {
242                    result.add(new HdmiPortInfo(id, cec.getType(), cec.getAddress(),
243                            cec.isCecSupported(), mhl.isMhlSupported(), cec.isArcSupported()));
244                    mhlInfoFound = true;
245                    break;
246                }
247            }
248            if (!mhlInfoFound) {
249                result.add(cec);
250            }
251        }
252
253        return Collections.unmodifiableList(result);
254    }
255
256    /**
257     * Returns HDMI port information for the given port id.
258     *
259     * @param portId HDMI port id
260     * @return {@link HdmiPortInfo} for the given port
261     */
262    HdmiPortInfo getPortInfo(int portId) {
263        // mPortInfo is an unmodifiable list and the only reference to its inner list.
264        // No lock is necessary.
265        for (HdmiPortInfo info : mPortInfo) {
266            if (portId == info.getId()) {
267                return info;
268            }
269        }
270        return null;
271    }
272
273    /**
274     * Returns the routing path (physical address) of the HDMI port for the given
275     * port id.
276     */
277    int portIdToPath(int portId) {
278        HdmiPortInfo portInfo = getPortInfo(portId);
279        if (portInfo == null) {
280            Slog.e(TAG, "Cannot find the port info: " + portId);
281            return HdmiConstants.INVALID_PHYSICAL_ADDRESS;
282        }
283        return portInfo.getAddress();
284    }
285
286    /**
287     * Returns the id of HDMI port located at the top of the hierarchy of
288     * the specified routing path. For the routing path 0x1220 (1.2.2.0), for instance,
289     * the port id to be returned is the ID associated with the port address
290     * 0x1000 (1.0.0.0) which is the topmost path of the given routing path.
291     */
292    int pathToPortId(int path) {
293        int portAddress = path & HdmiConstants.ROUTING_PATH_TOP_MASK;
294        for (HdmiPortInfo info : mPortInfo) {
295            if (portAddress == info.getAddress()) {
296                return info.getId();
297            }
298        }
299        return HdmiConstants.INVALID_PORT_ID;
300    }
301
302    /**
303     * Returns {@link Looper} for IO operation.
304     *
305     * <p>Declared as package-private.
306     */
307    Looper getIoLooper() {
308        return mIoThread.getLooper();
309    }
310
311    /**
312     * Returns {@link Looper} of main thread. Use this {@link Looper} instance
313     * for tasks that are running on main service thread.
314     *
315     * <p>Declared as package-private.
316     */
317    Looper getServiceLooper() {
318        return mHandler.getLooper();
319    }
320
321    /**
322     * Returns physical address of the device.
323     */
324    int getPhysicalAddress() {
325        return mCecController.getPhysicalAddress();
326    }
327
328    /**
329     * Returns vendor id of CEC service.
330     */
331    int getVendorId() {
332        return mCecController.getVendorId();
333    }
334
335    @ServiceThreadOnly
336    HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) {
337        assertRunOnServiceThread();
338        HdmiCecLocalDeviceTv tv = tv();
339        if (tv == null) {
340            return null;
341        }
342        return tv.getDeviceInfo(logicalAddress);
343    }
344
345    /**
346     * Returns version of CEC.
347     */
348    int getCecVersion() {
349        return mCecController.getVersion();
350    }
351
352    /**
353     * Whether a device of the specified physical address is connected to ARC enabled port.
354     */
355    boolean isConnectedToArcPort(int physicalAddress) {
356        for (HdmiPortInfo portInfo : mPortInfo) {
357            if (hasSameTopPort(portInfo.getAddress(), physicalAddress)
358                    && portInfo.isArcSupported()) {
359                return true;
360            }
361        }
362        return false;
363    }
364
365    void runOnServiceThread(Runnable runnable) {
366        mHandler.post(runnable);
367    }
368
369    void runOnServiceThreadAtFrontOfQueue(Runnable runnable) {
370        mHandler.postAtFrontOfQueue(runnable);
371    }
372
373    private void assertRunOnServiceThread() {
374        if (Looper.myLooper() != mHandler.getLooper()) {
375            throw new IllegalStateException("Should run on service thread.");
376        }
377    }
378
379    /**
380     * Transmit a CEC command to CEC bus.
381     *
382     * @param command CEC command to send out
383     * @param callback interface used to the result of send command
384     */
385    @ServiceThreadOnly
386    void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) {
387        assertRunOnServiceThread();
388        mCecController.sendCommand(command, callback);
389    }
390
391    @ServiceThreadOnly
392    void sendCecCommand(HdmiCecMessage command) {
393        assertRunOnServiceThread();
394        mCecController.sendCommand(command, null);
395    }
396
397    @ServiceThreadOnly
398    boolean handleCecCommand(HdmiCecMessage message) {
399        assertRunOnServiceThread();
400        return dispatchMessageToLocalDevice(message);
401    }
402
403    void setAudioReturnChannel(boolean enabled) {
404        mCecController.setAudioReturnChannel(enabled);
405    }
406
407    @ServiceThreadOnly
408    private boolean dispatchMessageToLocalDevice(HdmiCecMessage message) {
409        assertRunOnServiceThread();
410        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
411            if (device.dispatchMessage(message)
412                    && message.getDestination() != HdmiCec.ADDR_BROADCAST) {
413                return true;
414            }
415        }
416
417        Slog.w(TAG, "Unhandled cec command:" + message);
418        return false;
419    }
420
421    /**
422     * Called when a new hotplug event is issued.
423     *
424     * @param portNo hdmi port number where hot plug event issued.
425     * @param connected whether to be plugged in or not
426     */
427    @ServiceThreadOnly
428    void onHotplug(int portNo, boolean connected) {
429        assertRunOnServiceThread();
430        for (HdmiCecLocalDevice device : mCecController.getLocalDeviceList()) {
431            device.onHotplug(portNo, connected);
432        }
433        announceHotplugEvent(portNo, connected);
434    }
435
436    /**
437     * Poll all remote devices. It sends &lt;Polling Message&gt; to all remote
438     * devices.
439     *
440     * @param callback an interface used to get a list of all remote devices' address
441     * @param pickStrategy strategy how to pick polling candidates
442     * @param retryCount the number of retry used to send polling message to remote devices
443     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
444     */
445    @ServiceThreadOnly
446    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
447        assertRunOnServiceThread();
448        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
449    }
450
451    private int checkPollStrategy(int pickStrategy) {
452        int strategy = pickStrategy & HdmiConstants.POLL_STRATEGY_MASK;
453        if (strategy == 0) {
454            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
455        }
456        int iterationStrategy = pickStrategy & HdmiConstants.POLL_ITERATION_STRATEGY_MASK;
457        if (iterationStrategy == 0) {
458            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
459        }
460        return strategy | iterationStrategy;
461    }
462
463    List<HdmiCecLocalDevice> getAllLocalDevices() {
464        assertRunOnServiceThread();
465        return mCecController.getLocalDeviceList();
466    }
467
468    Object getServiceLock() {
469        return mLock;
470    }
471
472    void setAudioStatus(boolean mute, int volume) {
473        // TODO: Hook up with AudioManager.
474    }
475
476    void announceSystemAudioModeChange(boolean enabled) {
477        for (IHdmiSystemAudioModeChangeListener listener : mSystemAudioModeChangeListeners) {
478            invokeSystemAudioModeChange(listener, enabled);
479        }
480    }
481
482    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
483        // TODO: find better name instead of model name.
484        String displayName = Build.MODEL;
485        return new HdmiCecDeviceInfo(logicalAddress,
486                getPhysicalAddress(), deviceType, getVendorId(), displayName);
487    }
488
489    // Record class that monitors the event of the caller of being killed. Used to clean up
490    // the listener list and record list accordingly.
491    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
492        private final IHdmiHotplugEventListener mListener;
493
494        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
495            mListener = listener;
496        }
497
498        @Override
499        public void binderDied() {
500            synchronized (mLock) {
501                mHotplugEventListenerRecords.remove(this);
502                mHotplugEventListeners.remove(mListener);
503            }
504        }
505    }
506
507    private final class DeviceEventListenerRecord implements IBinder.DeathRecipient {
508        private final IHdmiDeviceEventListener mListener;
509
510        public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) {
511            mListener = listener;
512        }
513
514        @Override
515        public void binderDied() {
516            synchronized (mLock) {
517                mDeviceEventListenerRecords.remove(this);
518                mDeviceEventListeners.remove(mListener);
519            }
520        }
521    }
522
523    private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient {
524        private IHdmiSystemAudioModeChangeListener mListener;
525
526        public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) {
527            mListener = listener;
528        }
529
530        @Override
531        public void binderDied() {
532            synchronized (mLock) {
533                mSystemAudioModeChangeListenerRecords.remove(this);
534                mSystemAudioModeChangeListeners.remove(mListener);
535            }
536        }
537    }
538
539    private void enforceAccessPermission() {
540        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
541    }
542
543    private final class BinderService extends IHdmiControlService.Stub {
544        @Override
545        public int[] getSupportedTypes() {
546            enforceAccessPermission();
547            // mLocalDevices is an unmodifiable list - no lock necesary.
548            int[] localDevices = new int[mLocalDevices.size()];
549            for (int i = 0; i < localDevices.length; ++i) {
550                localDevices[i] = mLocalDevices.get(i);
551            }
552            return localDevices;
553        }
554
555        @Override
556        public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) {
557            enforceAccessPermission();
558            runOnServiceThread(new Runnable() {
559                @Override
560                public void run() {
561                    HdmiCecLocalDeviceTv tv = tv();
562                    if (tv == null) {
563                        Slog.w(TAG, "Local tv device not available");
564                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
565                        return;
566                    }
567                    tv.deviceSelect(logicalAddress, callback);
568                }
569            });
570        }
571
572        @Override
573        public void portSelect(final int portId, final IHdmiControlCallback callback) {
574            enforceAccessPermission();
575            runOnServiceThread(new Runnable() {
576                @Override
577                public void run() {
578                    HdmiCecLocalDeviceTv tv = tv();
579                    if (tv == null) {
580                        Slog.w(TAG, "Local tv device not available");
581                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
582                        return;
583                    }
584                    tv.portSelect(portId, callback);
585                }
586            });
587        }
588
589        @Override
590        public void sendKeyEvent(final int keyCode, final boolean isPressed) {
591            enforceAccessPermission();
592            runOnServiceThread(new Runnable() {
593                @Override
594                public void run() {
595                    // TODO: sendKeyEvent is for TV device only for now. Allow other
596                    //       local devices of different types to use this as well.
597                    HdmiCecLocalDeviceTv tv = tv();
598                    if (tv == null) {
599                        Slog.w(TAG, "Local tv device not available");
600                        return;
601                    }
602                    tv.sendKeyEvent(keyCode, isPressed);
603                }
604            });
605        }
606
607        @Override
608        public void oneTouchPlay(final IHdmiControlCallback callback) {
609            enforceAccessPermission();
610            runOnServiceThread(new Runnable() {
611                @Override
612                public void run() {
613                    HdmiControlService.this.oneTouchPlay(callback);
614                }
615            });
616        }
617
618        @Override
619        public void queryDisplayStatus(final IHdmiControlCallback callback) {
620            enforceAccessPermission();
621            runOnServiceThread(new Runnable() {
622                @Override
623                public void run() {
624                    HdmiControlService.this.queryDisplayStatus(callback);
625                }
626            });
627        }
628
629        @Override
630        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
631            enforceAccessPermission();
632            runOnServiceThread(new Runnable() {
633                @Override
634                public void run() {
635                    HdmiControlService.this.addHotplugEventListener(listener);
636                }
637            });
638        }
639
640        @Override
641        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
642            enforceAccessPermission();
643            runOnServiceThread(new Runnable() {
644                @Override
645                public void run() {
646                    HdmiControlService.this.removeHotplugEventListener(listener);
647                }
648            });
649        }
650
651        @Override
652        public void addDeviceEventListener(final IHdmiDeviceEventListener listener) {
653            enforceAccessPermission();
654            runOnServiceThread(new Runnable() {
655                @Override
656                public void run() {
657                    HdmiControlService.this.addDeviceEventListener(listener);
658                }
659            });
660        }
661
662        @Override
663        public List<HdmiPortInfo> getPortInfo() {
664            enforceAccessPermission();
665            return mPortInfo;
666        }
667
668        @Override
669        public boolean canChangeSystemAudioMode() {
670            enforceAccessPermission();
671            HdmiCecLocalDeviceTv tv = tv();
672            if (tv == null) {
673                return false;
674            }
675            return tv.canChangeSystemAudioMode();
676        }
677
678        @Override
679        public boolean getSystemAudioMode() {
680            enforceAccessPermission();
681            HdmiCecLocalDeviceTv tv = tv();
682            if (tv == null) {
683                return false;
684            }
685            return tv.getSystemAudioMode();
686        }
687
688        @Override
689        public void setSystemAudioMode(final boolean enabled, final IHdmiControlCallback callback) {
690            enforceAccessPermission();
691            runOnServiceThread(new Runnable() {
692                @Override
693                public void run() {
694                    HdmiCecLocalDeviceTv tv = tv();
695                    if (tv == null) {
696                        Slog.w(TAG, "Local tv device not available");
697                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
698                        return;
699                    }
700                    tv.changeSystemAudioMode(enabled, callback);
701                }
702            });
703        }
704
705        @Override
706        public void addSystemAudioModeChangeListener(
707                final IHdmiSystemAudioModeChangeListener listener) {
708            enforceAccessPermission();
709            HdmiControlService.this.addSystemAudioModeChangeListner(listener);
710        }
711
712        @Override
713        public void removeSystemAudioModeChangeListener(
714                final IHdmiSystemAudioModeChangeListener listener) {
715            enforceAccessPermission();
716            HdmiControlService.this.removeSystemAudioModeChangeListener(listener);
717        }
718    }
719
720    @ServiceThreadOnly
721    private void oneTouchPlay(final IHdmiControlCallback callback) {
722        assertRunOnServiceThread();
723        HdmiCecLocalDevicePlayback source = playback();
724        if (source == null) {
725            Slog.w(TAG, "Local playback device not available");
726            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
727            return;
728        }
729        source.oneTouchPlay(callback);
730    }
731
732    @ServiceThreadOnly
733    private void queryDisplayStatus(final IHdmiControlCallback callback) {
734        assertRunOnServiceThread();
735        HdmiCecLocalDevicePlayback source = playback();
736        if (source == null) {
737            Slog.w(TAG, "Local playback device not available");
738            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
739            return;
740        }
741        source.queryDisplayStatus(callback);
742    }
743
744    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
745        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
746        try {
747            listener.asBinder().linkToDeath(record, 0);
748        } catch (RemoteException e) {
749            Slog.w(TAG, "Listener already died");
750            return;
751        }
752        synchronized (mLock) {
753            mHotplugEventListenerRecords.add(record);
754            mHotplugEventListeners.add(listener);
755        }
756    }
757
758    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
759        synchronized (mLock) {
760            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
761                if (record.mListener.asBinder() == listener.asBinder()) {
762                    listener.asBinder().unlinkToDeath(record, 0);
763                    mHotplugEventListenerRecords.remove(record);
764                    break;
765                }
766            }
767            mHotplugEventListeners.remove(listener);
768        }
769    }
770
771    private void addDeviceEventListener(IHdmiDeviceEventListener listener) {
772        DeviceEventListenerRecord record = new DeviceEventListenerRecord(listener);
773        try {
774            listener.asBinder().linkToDeath(record, 0);
775        } catch (RemoteException e) {
776            Slog.w(TAG, "Listener already died");
777            return;
778        }
779        synchronized (mLock) {
780            mDeviceEventListeners.add(listener);
781            mDeviceEventListenerRecords.add(record);
782        }
783    }
784
785    void invokeDeviceEventListeners(HdmiCecDeviceInfo device, boolean activated) {
786        synchronized (mLock) {
787            for (IHdmiDeviceEventListener listener : mDeviceEventListeners) {
788                try {
789                    listener.onStatusChanged(device, activated);
790                } catch (RemoteException e) {
791                    Slog.e(TAG, "Failed to report device event:" + e);
792                }
793            }
794        }
795    }
796
797    private void addSystemAudioModeChangeListner(IHdmiSystemAudioModeChangeListener listener) {
798        SystemAudioModeChangeListenerRecord record = new SystemAudioModeChangeListenerRecord(
799                listener);
800        try {
801            listener.asBinder().linkToDeath(record, 0);
802        } catch (RemoteException e) {
803            Slog.w(TAG, "Listener already died");
804            return;
805        }
806        synchronized (mLock) {
807            mSystemAudioModeChangeListeners.add(listener);
808            mSystemAudioModeChangeListenerRecords.add(record);
809        }
810    }
811
812    private void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {
813        synchronized (mLock) {
814            for (SystemAudioModeChangeListenerRecord record :
815                    mSystemAudioModeChangeListenerRecords) {
816                if (record.mListener.asBinder() == listener) {
817                    listener.asBinder().unlinkToDeath(record, 0);
818                    mSystemAudioModeChangeListenerRecords.remove(record);
819                    break;
820                }
821            }
822            mSystemAudioModeChangeListeners.remove(listener);
823        }
824    }
825
826    private void invokeCallback(IHdmiControlCallback callback, int result) {
827        try {
828            callback.onComplete(result);
829        } catch (RemoteException e) {
830            Slog.e(TAG, "Invoking callback failed:" + e);
831        }
832    }
833
834    private void invokeSystemAudioModeChange(IHdmiSystemAudioModeChangeListener listener,
835            boolean enabled) {
836        try {
837            listener.onStatusChanged(enabled);
838        } catch (RemoteException e) {
839            Slog.e(TAG, "Invoking callback failed:" + e);
840        }
841    }
842
843    private void announceHotplugEvent(int portId, boolean connected) {
844        HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected);
845        synchronized (mLock) {
846            for (IHdmiHotplugEventListener listener : mHotplugEventListeners) {
847                invokeHotplugEventListenerLocked(listener, event);
848            }
849        }
850    }
851
852    private void invokeHotplugEventListenerLocked(IHdmiHotplugEventListener listener,
853            HdmiHotplugEvent event) {
854        try {
855            listener.onReceived(event);
856        } catch (RemoteException e) {
857            Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e);
858        }
859    }
860
861    private static boolean hasSameTopPort(int path1, int path2) {
862        return (path1 & HdmiConstants.ROUTING_PATH_TOP_MASK)
863                == (path2 & HdmiConstants.ROUTING_PATH_TOP_MASK);
864    }
865
866    private HdmiCecLocalDeviceTv tv() {
867        return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV);
868    }
869
870    private HdmiCecLocalDevicePlayback playback() {
871        return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
872    }
873}
874