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