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