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