HdmiControlService.java revision 41d974631c5f525da49c88d34cecedd5a4cfeda8
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 pickStrategy strategy how to pick polling candidates
452     * @param retryCount the number of retry used to send polling message to remote devices
453     * @throw IllegalArgumentException if {@code pickStrategy} is invalid value
454     */
455    @ServiceThreadOnly
456    void pollDevices(DevicePollingCallback callback, int pickStrategy, int retryCount) {
457        assertRunOnServiceThread();
458        mCecController.pollDevices(callback, checkPollStrategy(pickStrategy), retryCount);
459    }
460
461    private int checkPollStrategy(int pickStrategy) {
462        int strategy = pickStrategy & HdmiConstants.POLL_STRATEGY_MASK;
463        if (strategy == 0) {
464            throw new IllegalArgumentException("Invalid poll strategy:" + pickStrategy);
465        }
466        int iterationStrategy = pickStrategy & HdmiConstants.POLL_ITERATION_STRATEGY_MASK;
467        if (iterationStrategy == 0) {
468            throw new IllegalArgumentException("Invalid iteration strategy:" + pickStrategy);
469        }
470        return strategy | iterationStrategy;
471    }
472
473    List<HdmiCecLocalDevice> getAllLocalDevices() {
474        assertRunOnServiceThread();
475        return mCecController.getLocalDeviceList();
476    }
477
478    Object getServiceLock() {
479        return mLock;
480    }
481
482    void setAudioStatus(boolean mute, int volume) {
483        // TODO: Hook up with AudioManager.
484    }
485
486    void announceSystemAudioModeChange(boolean enabled) {
487        for (IHdmiSystemAudioModeChangeListener listener : mSystemAudioModeChangeListeners) {
488            invokeSystemAudioModeChange(listener, enabled);
489        }
490    }
491
492    private HdmiCecDeviceInfo createDeviceInfo(int logicalAddress, int deviceType) {
493        // TODO: find better name instead of model name.
494        String displayName = Build.MODEL;
495        return new HdmiCecDeviceInfo(logicalAddress,
496                getPhysicalAddress(), deviceType, getVendorId(), displayName);
497    }
498
499    // Record class that monitors the event of the caller of being killed. Used to clean up
500    // the listener list and record list accordingly.
501    private final class HotplugEventListenerRecord implements IBinder.DeathRecipient {
502        private final IHdmiHotplugEventListener mListener;
503
504        public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) {
505            mListener = listener;
506        }
507
508        @Override
509        public void binderDied() {
510            synchronized (mLock) {
511                mHotplugEventListenerRecords.remove(this);
512                mHotplugEventListeners.remove(mListener);
513            }
514        }
515    }
516
517    private final class DeviceEventListenerRecord implements IBinder.DeathRecipient {
518        private final IHdmiDeviceEventListener mListener;
519
520        public DeviceEventListenerRecord(IHdmiDeviceEventListener listener) {
521            mListener = listener;
522        }
523
524        @Override
525        public void binderDied() {
526            synchronized (mLock) {
527                mDeviceEventListenerRecords.remove(this);
528                mDeviceEventListeners.remove(mListener);
529            }
530        }
531    }
532
533    private final class SystemAudioModeChangeListenerRecord implements IBinder.DeathRecipient {
534        private IHdmiSystemAudioModeChangeListener mListener;
535
536        public SystemAudioModeChangeListenerRecord(IHdmiSystemAudioModeChangeListener listener) {
537            mListener = listener;
538        }
539
540        @Override
541        public void binderDied() {
542            synchronized (mLock) {
543                mSystemAudioModeChangeListenerRecords.remove(this);
544                mSystemAudioModeChangeListeners.remove(mListener);
545            }
546        }
547    }
548
549    private void enforceAccessPermission() {
550        getContext().enforceCallingOrSelfPermission(PERMISSION, TAG);
551    }
552
553    private final class BinderService extends IHdmiControlService.Stub {
554        @Override
555        public int[] getSupportedTypes() {
556            enforceAccessPermission();
557            // mLocalDevices is an unmodifiable list - no lock necesary.
558            int[] localDevices = new int[mLocalDevices.size()];
559            for (int i = 0; i < localDevices.length; ++i) {
560                localDevices[i] = mLocalDevices.get(i);
561            }
562            return localDevices;
563        }
564
565        @Override
566        public void deviceSelect(final int logicalAddress, final IHdmiControlCallback callback) {
567            enforceAccessPermission();
568            runOnServiceThread(new Runnable() {
569                @Override
570                public void run() {
571                    HdmiCecLocalDeviceTv tv = tv();
572                    if (tv == null) {
573                        Slog.w(TAG, "Local tv device not available");
574                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
575                        return;
576                    }
577                    tv.deviceSelect(logicalAddress, callback);
578                }
579            });
580        }
581
582        @Override
583        public void portSelect(final int portId, final IHdmiControlCallback callback) {
584            enforceAccessPermission();
585            runOnServiceThread(new Runnable() {
586                @Override
587                public void run() {
588                    HdmiCecLocalDeviceTv tv = tv();
589                    if (tv == null) {
590                        Slog.w(TAG, "Local tv device not available");
591                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
592                        return;
593                    }
594                    tv.doManualPortSwitching(portId, callback);
595                }
596            });
597        }
598
599        @Override
600        public void sendKeyEvent(final int keyCode, final boolean isPressed) {
601            enforceAccessPermission();
602            runOnServiceThread(new Runnable() {
603                @Override
604                public void run() {
605                    // TODO: sendKeyEvent is for TV device only for now. Allow other
606                    //       local devices of different types to use this as well.
607                    HdmiCecLocalDeviceTv tv = tv();
608                    if (tv == null) {
609                        Slog.w(TAG, "Local tv device not available");
610                        return;
611                    }
612                    tv.sendKeyEvent(keyCode, isPressed);
613                }
614            });
615        }
616
617        @Override
618        public void oneTouchPlay(final IHdmiControlCallback callback) {
619            enforceAccessPermission();
620            runOnServiceThread(new Runnable() {
621                @Override
622                public void run() {
623                    HdmiControlService.this.oneTouchPlay(callback);
624                }
625            });
626        }
627
628        @Override
629        public void queryDisplayStatus(final IHdmiControlCallback callback) {
630            enforceAccessPermission();
631            runOnServiceThread(new Runnable() {
632                @Override
633                public void run() {
634                    HdmiControlService.this.queryDisplayStatus(callback);
635                }
636            });
637        }
638
639        @Override
640        public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
641            enforceAccessPermission();
642            runOnServiceThread(new Runnable() {
643                @Override
644                public void run() {
645                    HdmiControlService.this.addHotplugEventListener(listener);
646                }
647            });
648        }
649
650        @Override
651        public void removeHotplugEventListener(final IHdmiHotplugEventListener listener) {
652            enforceAccessPermission();
653            runOnServiceThread(new Runnable() {
654                @Override
655                public void run() {
656                    HdmiControlService.this.removeHotplugEventListener(listener);
657                }
658            });
659        }
660
661        @Override
662        public void addDeviceEventListener(final IHdmiDeviceEventListener listener) {
663            enforceAccessPermission();
664            runOnServiceThread(new Runnable() {
665                @Override
666                public void run() {
667                    HdmiControlService.this.addDeviceEventListener(listener);
668                }
669            });
670        }
671
672        @Override
673        public List<HdmiPortInfo> getPortInfo() {
674            enforceAccessPermission();
675            return mPortInfo;
676        }
677
678        @Override
679        public boolean canChangeSystemAudioMode() {
680            enforceAccessPermission();
681            HdmiCecLocalDeviceTv tv = tv();
682            if (tv == null) {
683                return false;
684            }
685            return tv.hasSystemAudioDevice();
686        }
687
688        @Override
689        public boolean getSystemAudioMode() {
690            enforceAccessPermission();
691            HdmiCecLocalDeviceTv tv = tv();
692            if (tv == null) {
693                return false;
694            }
695            return tv.getSystemAudioMode();
696        }
697
698        @Override
699        public void setSystemAudioMode(final boolean enabled, final IHdmiControlCallback callback) {
700            enforceAccessPermission();
701            runOnServiceThread(new Runnable() {
702                @Override
703                public void run() {
704                    HdmiCecLocalDeviceTv tv = tv();
705                    if (tv == null) {
706                        Slog.w(TAG, "Local tv device not available");
707                        invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
708                        return;
709                    }
710                    tv.changeSystemAudioMode(enabled, callback);
711                }
712            });
713        }
714
715        @Override
716        public void addSystemAudioModeChangeListener(
717                final IHdmiSystemAudioModeChangeListener listener) {
718            enforceAccessPermission();
719            HdmiControlService.this.addSystemAudioModeChangeListner(listener);
720        }
721
722        @Override
723        public void removeSystemAudioModeChangeListener(
724                final IHdmiSystemAudioModeChangeListener listener) {
725            enforceAccessPermission();
726            HdmiControlService.this.removeSystemAudioModeChangeListener(listener);
727        }
728
729        @Override
730        public void setControlEnabled(final boolean enabled) {
731            enforceAccessPermission();
732            synchronized (mLock) {
733                mHdmiControlEnabled = enabled;
734            }
735            // TODO: Stop the running actions when disabled, and start
736            //       address allocation/device discovery when enabled.
737            if (!enabled) {
738                return;
739            }
740            runOnServiceThread(new Runnable() {
741                @Override
742                public void run() {
743                    HdmiCecLocalDeviceTv tv = tv();
744                    if (tv == null) {
745                        return;
746                    }
747                    int value = enabled ? HdmiCec.ENABLED : HdmiCec.DISABLED;
748                    mCecController.setOption(HdmiCec.OPTION_CEC_ENABLE, value);
749                    if (mMhlController != null) {
750                        mMhlController.setOption(HdmiCec.OPTION_MHL_ENABLE, value);
751                    }
752                    tv.routingAtEnableTime();
753                }
754            });
755        }
756
757        @Override
758        public void setSystemAudioVolume(final int oldIndex, final int newIndex,
759                final int maxIndex) {
760            enforceAccessPermission();
761            runOnServiceThread(new Runnable() {
762                @Override
763                public void run() {
764                    HdmiCecLocalDeviceTv tv = tv();
765                    if (tv == null) {
766                        Slog.w(TAG, "Local tv device not available");
767                        return;
768                    }
769                    tv.changeVolume(oldIndex, newIndex - oldIndex, maxIndex);
770                }
771            });
772        }
773
774        @Override
775        public void setSystemAudioMute(final boolean mute) {
776            enforceAccessPermission();
777            runOnServiceThread(new Runnable() {
778                @Override
779                public void run() {
780                    HdmiCecLocalDeviceTv tv = tv();
781                    if (tv == null) {
782                        Slog.w(TAG, "Local tv device not available");
783                        return;
784                    }
785                    tv.changeMute(mute);
786                }
787            });
788        }
789
790        @Override
791        public void setArcMode(final boolean enabled) {
792            enforceAccessPermission();
793            runOnServiceThread(new Runnable() {
794                @Override
795                public void run() {
796                    HdmiCecLocalDeviceTv tv = tv();
797                    if (tv == null) {
798                        Log.w(TAG, "Local tv device not available to change arc mode.");
799                        return;
800                    }
801                }
802            });
803        }
804
805        @Override
806        public void setOption(final int key, final int value) {
807            if (!isTvDevice()) {
808                return;
809            }
810            switch (key) {
811                case HdmiCec.OPTION_CEC_AUTO_WAKEUP:
812                    mCecController.setOption(key, value);
813                    break;
814                case HdmiCec.OPTION_CEC_AUTO_DEVICE_OFF:
815                    // No need to pass this option to HAL.
816                    tv().setAutoDeviceOff(value == HdmiCec.ENABLED);
817                    break;
818                case HdmiCec.OPTION_MHL_INPUT_SWITCHING:  // Fall through
819                case HdmiCec.OPTION_MHL_POWER_CHARGE:
820                    if (mMhlController != null) {
821                        mMhlController.setOption(key, value);
822                    }
823                    break;
824            }
825        }
826
827        private boolean isTvDevice() {
828            return tv() != null;
829        }
830    }
831
832    @ServiceThreadOnly
833    private void oneTouchPlay(final IHdmiControlCallback callback) {
834        assertRunOnServiceThread();
835        HdmiCecLocalDevicePlayback source = playback();
836        if (source == null) {
837            Slog.w(TAG, "Local playback device not available");
838            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
839            return;
840        }
841        source.oneTouchPlay(callback);
842    }
843
844    @ServiceThreadOnly
845    private void queryDisplayStatus(final IHdmiControlCallback callback) {
846        assertRunOnServiceThread();
847        HdmiCecLocalDevicePlayback source = playback();
848        if (source == null) {
849            Slog.w(TAG, "Local playback device not available");
850            invokeCallback(callback, HdmiCec.RESULT_SOURCE_NOT_AVAILABLE);
851            return;
852        }
853        source.queryDisplayStatus(callback);
854    }
855
856    private void addHotplugEventListener(IHdmiHotplugEventListener listener) {
857        HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
858        try {
859            listener.asBinder().linkToDeath(record, 0);
860        } catch (RemoteException e) {
861            Slog.w(TAG, "Listener already died");
862            return;
863        }
864        synchronized (mLock) {
865            mHotplugEventListenerRecords.add(record);
866            mHotplugEventListeners.add(listener);
867        }
868    }
869
870    private void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
871        synchronized (mLock) {
872            for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) {
873                if (record.mListener.asBinder() == listener.asBinder()) {
874                    listener.asBinder().unlinkToDeath(record, 0);
875                    mHotplugEventListenerRecords.remove(record);
876                    break;
877                }
878            }
879            mHotplugEventListeners.remove(listener);
880        }
881    }
882
883    private void addDeviceEventListener(IHdmiDeviceEventListener listener) {
884        DeviceEventListenerRecord record = new DeviceEventListenerRecord(listener);
885        try {
886            listener.asBinder().linkToDeath(record, 0);
887        } catch (RemoteException e) {
888            Slog.w(TAG, "Listener already died");
889            return;
890        }
891        synchronized (mLock) {
892            mDeviceEventListeners.add(listener);
893            mDeviceEventListenerRecords.add(record);
894        }
895    }
896
897    void invokeDeviceEventListeners(HdmiCecDeviceInfo device, boolean activated) {
898        synchronized (mLock) {
899            for (IHdmiDeviceEventListener listener : mDeviceEventListeners) {
900                try {
901                    listener.onStatusChanged(device, activated);
902                } catch (RemoteException e) {
903                    Slog.e(TAG, "Failed to report device event:" + e);
904                }
905            }
906        }
907    }
908
909    private void addSystemAudioModeChangeListner(IHdmiSystemAudioModeChangeListener listener) {
910        SystemAudioModeChangeListenerRecord record = new SystemAudioModeChangeListenerRecord(
911                listener);
912        try {
913            listener.asBinder().linkToDeath(record, 0);
914        } catch (RemoteException e) {
915            Slog.w(TAG, "Listener already died");
916            return;
917        }
918        synchronized (mLock) {
919            mSystemAudioModeChangeListeners.add(listener);
920            mSystemAudioModeChangeListenerRecords.add(record);
921        }
922    }
923
924    private void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {
925        synchronized (mLock) {
926            for (SystemAudioModeChangeListenerRecord record :
927                    mSystemAudioModeChangeListenerRecords) {
928                if (record.mListener.asBinder() == listener) {
929                    listener.asBinder().unlinkToDeath(record, 0);
930                    mSystemAudioModeChangeListenerRecords.remove(record);
931                    break;
932                }
933            }
934            mSystemAudioModeChangeListeners.remove(listener);
935        }
936    }
937
938    private void invokeCallback(IHdmiControlCallback callback, int result) {
939        try {
940            callback.onComplete(result);
941        } catch (RemoteException e) {
942            Slog.e(TAG, "Invoking callback failed:" + e);
943        }
944    }
945
946    private void invokeSystemAudioModeChange(IHdmiSystemAudioModeChangeListener listener,
947            boolean enabled) {
948        try {
949            listener.onStatusChanged(enabled);
950        } catch (RemoteException e) {
951            Slog.e(TAG, "Invoking callback failed:" + e);
952        }
953    }
954
955    private void announceHotplugEvent(int portId, boolean connected) {
956        HdmiHotplugEvent event = new HdmiHotplugEvent(portId, connected);
957        synchronized (mLock) {
958            for (IHdmiHotplugEventListener listener : mHotplugEventListeners) {
959                invokeHotplugEventListenerLocked(listener, event);
960            }
961        }
962    }
963
964    private void invokeHotplugEventListenerLocked(IHdmiHotplugEventListener listener,
965            HdmiHotplugEvent event) {
966        try {
967            listener.onReceived(event);
968        } catch (RemoteException e) {
969            Slog.e(TAG, "Failed to report hotplug event:" + event.toString(), e);
970        }
971    }
972
973    private static boolean hasSameTopPort(int path1, int path2) {
974        return (path1 & HdmiConstants.ROUTING_PATH_TOP_MASK)
975                == (path2 & HdmiConstants.ROUTING_PATH_TOP_MASK);
976    }
977
978    private HdmiCecLocalDeviceTv tv() {
979        return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiCec.DEVICE_TV);
980    }
981
982    private HdmiCecLocalDevicePlayback playback() {
983        return (HdmiCecLocalDevicePlayback) mCecController.getLocalDevice(HdmiCec.DEVICE_PLAYBACK);
984    }
985
986    AudioManager getAudioManager() {
987        return (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
988    }
989
990    boolean isControlEnabled() {
991        synchronized (mLock) {
992            return mHdmiControlEnabled;
993        }
994    }
995}
996