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