TvInputHardwareManager.java revision 7474faca5bab2fba5749cff7ed15558231307e38
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.tv;
18
19import static android.media.tv.TvInputManager.INPUT_STATE_CONNECTED;
20import static android.media.tv.TvInputManager.INPUT_STATE_DISCONNECTED;
21
22import android.content.Context;
23import android.hardware.hdmi.HdmiCecDeviceInfo;
24import android.hardware.hdmi.HdmiHotplugEvent;
25import android.hardware.hdmi.IHdmiControlService;
26import android.hardware.hdmi.IHdmiDeviceEventListener;
27import android.hardware.hdmi.IHdmiHotplugEventListener;
28import android.hardware.hdmi.IHdmiInputChangeListener;
29import android.media.AudioDevicePort;
30import android.media.AudioManager;
31import android.media.AudioPatch;
32import android.media.AudioPort;
33import android.media.AudioPortConfig;
34import android.media.tv.ITvInputHardware;
35import android.media.tv.ITvInputHardwareCallback;
36import android.media.tv.TvInputHardwareInfo;
37import android.media.tv.TvInputInfo;
38import android.media.tv.TvStreamConfig;
39import android.os.Handler;
40import android.os.IBinder;
41import android.os.Looper;
42import android.os.Message;
43import android.os.RemoteException;
44import android.os.ServiceManager;
45import android.util.Slog;
46import android.util.SparseArray;
47import android.util.SparseBooleanArray;
48import android.view.KeyEvent;
49import android.view.Surface;
50
51import com.android.server.SystemService;
52
53import java.util.ArrayList;
54import java.util.HashSet;
55import java.util.List;
56import java.util.Set;
57
58/**
59 * A helper class for TvInputManagerService to handle TV input hardware.
60 *
61 * This class does a basic connection management and forwarding calls to TvInputHal which eventually
62 * calls to tv_input HAL module.
63 *
64 * @hide
65 */
66class TvInputHardwareManager implements TvInputHal.Callback {
67    private static final String TAG = TvInputHardwareManager.class.getSimpleName();
68    private final TvInputHal mHal = new TvInputHal(this);
69    private final SparseArray<Connection> mConnections = new SparseArray<Connection>();
70    private final List<TvInputHardwareInfo> mInfoList = new ArrayList<TvInputHardwareInfo>();
71    private final Context mContext;
72    private final Listener mListener;
73    private final Set<Integer> mActiveHdmiSources = new HashSet<Integer>();
74    private final AudioManager mAudioManager;
75    private final SparseBooleanArray mHdmiStateMap = new SparseBooleanArray();
76    // TODO: Should handle INACTIVE case.
77    private final SparseArray<TvInputInfo> mTvInputInfoMap = new SparseArray<TvInputInfo>();
78    private final IHdmiDeviceEventListener mHdmiDeviceEventListener = new HdmiDeviceEventListener();
79    private final IHdmiHotplugEventListener mHdmiHotplugEventListener =
80            new HdmiHotplugEventListener();
81    private final IHdmiInputChangeListener mHdmiInputChangeListener = new HdmiInputChangeListener();
82
83    // Calls to mListener should happen here.
84    private final Handler mHandler = new ListenerHandler();
85
86    private final Object mLock = new Object();
87
88    public TvInputHardwareManager(Context context, Listener listener) {
89        mContext = context;
90        mListener = listener;
91        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
92        mHal.init();
93    }
94
95    public void onBootPhase(int phase) {
96        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
97            IHdmiControlService hdmiControlService = IHdmiControlService.Stub.asInterface(
98                    ServiceManager.getService(Context.HDMI_CONTROL_SERVICE));
99            if (hdmiControlService != null) {
100                try {
101                    hdmiControlService.addHotplugEventListener(mHdmiHotplugEventListener);
102                    hdmiControlService.addDeviceEventListener(mHdmiDeviceEventListener);
103                    hdmiControlService.setInputChangeListener(mHdmiInputChangeListener);
104                } catch (RemoteException e) {
105                    Slog.w(TAG, "Error registering listeners to HdmiControlService:" + e);
106                }
107            }
108        }
109    }
110
111    @Override
112    public void onDeviceAvailable(
113            TvInputHardwareInfo info, TvStreamConfig[] configs) {
114        synchronized (mLock) {
115            Connection connection = new Connection(info);
116            connection.updateConfigsLocked(configs);
117            mConnections.put(info.getDeviceId(), connection);
118            buildInfoListLocked();
119            mHandler.obtainMessage(
120                    ListenerHandler.HARDWARE_DEVICE_ADDED, 0, 0, info).sendToTarget();
121        }
122    }
123
124    private void buildInfoListLocked() {
125        mInfoList.clear();
126        for (int i = 0; i < mConnections.size(); ++i) {
127            mInfoList.add(mConnections.valueAt(i).getHardwareInfoLocked());
128        }
129    }
130
131    @Override
132    public void onDeviceUnavailable(int deviceId) {
133        synchronized (mLock) {
134            Connection connection = mConnections.get(deviceId);
135            if (connection == null) {
136                Slog.e(TAG, "onDeviceUnavailable: Cannot find a connection with " + deviceId);
137                return;
138            }
139            connection.resetLocked(null, null, null, null, null);
140            mConnections.remove(deviceId);
141            buildInfoListLocked();
142            mHandler.obtainMessage(
143                    ListenerHandler.HARDWARE_DEVICE_REMOVED, deviceId, 0).sendToTarget();
144        }
145    }
146
147    @Override
148    public void onStreamConfigurationChanged(int deviceId, TvStreamConfig[] configs) {
149        synchronized (mLock) {
150            Connection connection = mConnections.get(deviceId);
151            if (connection == null) {
152                Slog.e(TAG, "StreamConfigurationChanged: Cannot find a connection with "
153                        + deviceId);
154                return;
155            }
156            connection.updateConfigsLocked(configs);
157            try {
158                connection.getCallbackLocked().onStreamConfigChanged(configs);
159            } catch (RemoteException e) {
160                Slog.e(TAG, "onStreamConfigurationChanged: " + e);
161            }
162        }
163    }
164
165    public List<TvInputHardwareInfo> getHardwareList() {
166        synchronized (mLock) {
167            return mInfoList;
168        }
169    }
170
171    private boolean checkUidChangedLocked(
172            Connection connection, int callingUid, int resolvedUserId) {
173        Integer connectionCallingUid = connection.getCallingUidLocked();
174        Integer connectionResolvedUserId = connection.getResolvedUserIdLocked();
175        if (connectionCallingUid == null || connectionResolvedUserId == null) {
176            return true;
177        }
178        if (connectionCallingUid != callingUid || connectionResolvedUserId != resolvedUserId) {
179            return true;
180        }
181        return false;
182    }
183
184    private int convertConnectedToState(boolean connected) {
185        if (connected) {
186            return INPUT_STATE_CONNECTED;
187        } else {
188            return INPUT_STATE_DISCONNECTED;
189        }
190    }
191
192    public void registerTvInputInfo(TvInputInfo info, int deviceId) {
193        if (info.getType() == TvInputInfo.TYPE_VIRTUAL) {
194            throw new IllegalArgumentException("info (" + info + ") has virtual type.");
195        }
196        synchronized (mLock) {
197            if (mTvInputInfoMap.indexOfKey(deviceId) >= 0) {
198                Slog.w(TAG, "Trying to override previous registration: old = "
199                        + mTvInputInfoMap.get(deviceId) + ":" + deviceId + ", new = "
200                        + info + ":" + deviceId);
201            }
202            mTvInputInfoMap.put(deviceId, info);
203
204            for (int i = 0; i < mHdmiStateMap.size(); ++i) {
205                String inputId = findInputIdForHdmiPortLocked(mHdmiStateMap.keyAt(i));
206                if (inputId != null && inputId.equals(info.getId())) {
207                    mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
208                            convertConnectedToState(mHdmiStateMap.valueAt(i)), 0,
209                            inputId).sendToTarget();
210                }
211            }
212        }
213    }
214
215    /**
216     * Create a TvInputHardware object with a specific deviceId. One service at a time can access
217     * the object, and if more than one process attempts to create hardware with the same deviceId,
218     * the latest service will get the object and all the other hardware are released. The
219     * release is notified via ITvInputHardwareCallback.onReleased().
220     */
221    public ITvInputHardware acquireHardware(int deviceId, ITvInputHardwareCallback callback,
222            TvInputInfo info, int callingUid, int resolvedUserId) {
223        if (callback == null) {
224            throw new NullPointerException();
225        }
226        synchronized (mLock) {
227            Connection connection = mConnections.get(deviceId);
228            if (connection == null) {
229                Slog.e(TAG, "Invalid deviceId : " + deviceId);
230                return null;
231            }
232            if (checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
233                TvInputHardwareImpl hardware =
234                        new TvInputHardwareImpl(connection.getHardwareInfoLocked());
235                try {
236                    callback.asBinder().linkToDeath(connection, 0);
237                } catch (RemoteException e) {
238                    hardware.release();
239                    return null;
240                }
241                connection.resetLocked(hardware, callback, info, callingUid, resolvedUserId);
242            }
243            return connection.getHardwareLocked();
244        }
245    }
246
247    /**
248     * Release the specified hardware.
249     */
250    public void releaseHardware(int deviceId, ITvInputHardware hardware, int callingUid,
251            int resolvedUserId) {
252        synchronized (mLock) {
253            Connection connection = mConnections.get(deviceId);
254            if (connection == null) {
255                Slog.e(TAG, "Invalid deviceId : " + deviceId);
256                return;
257            }
258            if (connection.getHardwareLocked() != hardware
259                    || checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
260                return;
261            }
262            connection.resetLocked(null, null, null, null, null);
263        }
264    }
265
266    private String findInputIdForHdmiPortLocked(int port) {
267        for (TvInputHardwareInfo hardwareInfo : mInfoList) {
268            if (hardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI
269                    && hardwareInfo.getHdmiPortId() == port) {
270                TvInputInfo info = mTvInputInfoMap.get(hardwareInfo.getDeviceId());
271                return (info == null) ? null : info.getId();
272            }
273        }
274        return null;
275    }
276
277    private class Connection implements IBinder.DeathRecipient {
278        private final TvInputHardwareInfo mHardwareInfo;
279        private TvInputInfo mInfo;
280        private TvInputHardwareImpl mHardware = null;
281        private ITvInputHardwareCallback mCallback;
282        private TvStreamConfig[] mConfigs = null;
283        private Integer mCallingUid = null;
284        private Integer mResolvedUserId = null;
285
286        public Connection(TvInputHardwareInfo hardwareInfo) {
287            mHardwareInfo = hardwareInfo;
288        }
289
290        // *Locked methods assume TvInputHardwareManager.mLock is held.
291
292        public void resetLocked(TvInputHardwareImpl hardware, ITvInputHardwareCallback callback,
293                TvInputInfo info, Integer callingUid, Integer resolvedUserId) {
294            if (mHardware != null) {
295                try {
296                    mCallback.onReleased();
297                } catch (RemoteException e) {
298                    Slog.e(TAG, "Connection::resetHardware: " + e);
299                }
300                mHardware.release();
301            }
302            mHardware = hardware;
303            mCallback = callback;
304            mInfo = info;
305            mCallingUid = callingUid;
306            mResolvedUserId = resolvedUserId;
307
308            if (mHardware != null && mCallback != null) {
309                try {
310                    mCallback.onStreamConfigChanged(getConfigsLocked());
311                } catch (RemoteException e) {
312                    Slog.e(TAG, "Connection::resetHardware: " + e);
313                }
314            }
315        }
316
317        public void updateConfigsLocked(TvStreamConfig[] configs) {
318            mConfigs = configs;
319        }
320
321        public TvInputHardwareInfo getHardwareInfoLocked() {
322            return mHardwareInfo;
323        }
324
325        public TvInputInfo getInfoLocked() {
326            return mInfo;
327        }
328
329        public ITvInputHardware getHardwareLocked() {
330            return mHardware;
331        }
332
333        public ITvInputHardwareCallback getCallbackLocked() {
334            return mCallback;
335        }
336
337        public TvStreamConfig[] getConfigsLocked() {
338            return mConfigs;
339        }
340
341        public Integer getCallingUidLocked() {
342            return mCallingUid;
343        }
344
345        public Integer getResolvedUserIdLocked() {
346            return mResolvedUserId;
347        }
348
349        @Override
350        public void binderDied() {
351            synchronized (mLock) {
352                resetLocked(null, null, null, null, null);
353            }
354        }
355    }
356
357    private class TvInputHardwareImpl extends ITvInputHardware.Stub {
358        private final TvInputHardwareInfo mInfo;
359        private boolean mReleased = false;
360        private final Object mImplLock = new Object();
361
362        private final AudioDevicePort mAudioSource;
363        private final AudioDevicePort mAudioSink;
364        private AudioPatch mAudioPatch = null;
365
366        private TvStreamConfig mActiveConfig = null;
367
368        public TvInputHardwareImpl(TvInputHardwareInfo info) {
369            mInfo = info;
370            AudioDevicePort audioSource = null;
371            AudioDevicePort audioSink = null;
372            if (mInfo.getAudioType() != AudioManager.DEVICE_NONE) {
373                ArrayList<AudioPort> devicePorts = new ArrayList<AudioPort>();
374                if (mAudioManager.listAudioDevicePorts(devicePorts) == AudioManager.SUCCESS) {
375                    // Find source
376                    for (AudioPort port : devicePorts) {
377                        AudioDevicePort devicePort = (AudioDevicePort) port;
378                        if (devicePort.type() == mInfo.getAudioType() &&
379                                devicePort.address().equals(mInfo.getAudioAddress())) {
380                            audioSource = devicePort;
381                            break;
382                        }
383                    }
384                    // Find sink
385                    // TODO: App may want to specify sink device?
386                    int sinkDevices = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
387                    for (AudioPort port : devicePorts) {
388                        AudioDevicePort devicePort = (AudioDevicePort) port;
389                        if (devicePort.type() == sinkDevices) {
390                            audioSink = devicePort;
391                            break;
392                        }
393                    }
394                }
395            }
396            mAudioSource = audioSource;
397            mAudioSink = audioSink;
398        }
399
400        public void release() {
401            synchronized (mImplLock) {
402                if (mAudioPatch != null) {
403                    mAudioManager.releaseAudioPatch(mAudioPatch);
404                    mAudioPatch = null;
405                }
406                mReleased = true;
407            }
408        }
409
410        // A TvInputHardwareImpl object holds only one active session. Therefore, if a client
411        // attempts to call setSurface with different TvStreamConfig objects, the last call will
412        // prevail.
413        @Override
414        public boolean setSurface(Surface surface, TvStreamConfig config)
415                throws RemoteException {
416            synchronized (mImplLock) {
417                if (mReleased) {
418                    throw new IllegalStateException("Device already released.");
419                }
420                if (surface != null && config == null) {
421                    return false;
422                }
423                if (surface == null && mActiveConfig == null) {
424                    return false;
425                }
426                if (mInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
427                    if (surface != null) {
428                        // Set "Active Source" for HDMI.
429                        // TODO(hdmi): mHdmiClient.deviceSelect(...);
430                        mActiveHdmiSources.add(mInfo.getDeviceId());
431                    } else {
432                        mActiveHdmiSources.remove(mInfo.getDeviceId());
433                        if (mActiveHdmiSources.size() == 0) {
434                            // Tell HDMI that no HDMI source is active
435                            // TODO(hdmi): mHdmiClient.portSelect(null);
436                        }
437                    }
438                }
439                if (mAudioSource != null && mAudioSink != null) {
440                    if (surface != null) {
441                        AudioPortConfig sourceConfig = mAudioSource.activeConfig();
442                        AudioPortConfig sinkConfig = mAudioSink.activeConfig();
443                        AudioPatch[] audioPatchArray = new AudioPatch[] { mAudioPatch };
444                        // TODO: build config if activeConfig() == null
445                        mAudioManager.createAudioPatch(
446                                audioPatchArray,
447                                new AudioPortConfig[] { sourceConfig },
448                                new AudioPortConfig[] { sinkConfig });
449                        mAudioPatch = audioPatchArray[0];
450                    } else {
451                        mAudioManager.releaseAudioPatch(mAudioPatch);
452                        mAudioPatch = null;
453                    }
454                }
455                int result = TvInputHal.ERROR_UNKNOWN;
456                if (surface == null) {
457                    result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
458                    mActiveConfig = null;
459                } else {
460                    if (config != mActiveConfig && mActiveConfig != null) {
461                        result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
462                        if (result != TvInputHal.SUCCESS) {
463                            mActiveConfig = null;
464                            return false;
465                        }
466                    }
467                    result = mHal.addStream(mInfo.getDeviceId(), surface, config);
468                    if (result == TvInputHal.SUCCESS) {
469                        mActiveConfig = config;
470                    }
471                }
472                return result == TvInputHal.SUCCESS;
473            }
474        }
475
476        @Override
477        public void setVolume(float volume) throws RemoteException {
478            synchronized (mImplLock) {
479                if (mReleased) {
480                    throw new IllegalStateException("Device already released.");
481                }
482            }
483            // TODO: Use AudioGain?
484        }
485
486        @Override
487        public boolean dispatchKeyEventToHdmi(KeyEvent event) throws RemoteException {
488            synchronized (mImplLock) {
489                if (mReleased) {
490                    throw new IllegalStateException("Device already released.");
491                }
492            }
493            if (mInfo.getType() != TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) {
494                return false;
495            }
496            // TODO(hdmi): mHdmiClient.sendKeyEvent(event);
497            return false;
498        }
499    }
500
501    interface Listener {
502        public void onStateChanged(String inputId, int state);
503        public void onHardwareDeviceAdded(TvInputHardwareInfo info);
504        public void onHardwareDeviceRemoved(int deviceId);
505        public void onHdmiCecDeviceAdded(HdmiCecDeviceInfo cecDevice);
506        public void onHdmiCecDeviceRemoved(HdmiCecDeviceInfo cecDevice);
507    }
508
509    private class ListenerHandler extends Handler {
510        private static final int STATE_CHANGED = 1;
511        private static final int HARDWARE_DEVICE_ADDED = 2;
512        private static final int HARDWARE_DEVICE_REMOVED = 3;
513        private static final int CEC_DEVICE_ADDED = 4;
514        private static final int CEC_DEVICE_REMOVED = 5;
515
516        @Override
517        public final void handleMessage(Message msg) {
518            switch (msg.what) {
519                case STATE_CHANGED: {
520                    String inputId = (String) msg.obj;
521                    int state = msg.arg1;
522                    mListener.onStateChanged(inputId, state);
523                    break;
524                }
525                case HARDWARE_DEVICE_ADDED: {
526                    TvInputHardwareInfo info = (TvInputHardwareInfo) msg.obj;
527                    mListener.onHardwareDeviceAdded(info);
528                    break;
529                }
530                case HARDWARE_DEVICE_REMOVED: {
531                    int deviceId = msg.arg1;
532                    mListener.onHardwareDeviceRemoved(deviceId);
533                    break;
534                }
535                case CEC_DEVICE_ADDED: {
536                    HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
537                    mListener.onHdmiCecDeviceAdded(info);
538                    break;
539                }
540                case CEC_DEVICE_REMOVED: {
541                    HdmiCecDeviceInfo info = (HdmiCecDeviceInfo) msg.obj;
542                    mListener.onHdmiCecDeviceRemoved(info);
543                    break;
544                }
545                default: {
546                    Slog.w(TAG, "Unhandled message: " + msg);
547                    break;
548                }
549            }
550        }
551    }
552
553    // Listener implementations for HdmiControlService
554
555    private final class HdmiHotplugEventListener extends IHdmiHotplugEventListener.Stub {
556        @Override
557        public void onReceived(HdmiHotplugEvent event) {
558            synchronized (mLock) {
559                mHdmiStateMap.put(event.getPort(), event.isConnected());
560                String inputId = findInputIdForHdmiPortLocked(event.getPort());
561                if (inputId == null) {
562                    return;
563                }
564                mHandler.obtainMessage(ListenerHandler.STATE_CHANGED,
565                        convertConnectedToState(event.isConnected()), 0, inputId).sendToTarget();
566            }
567        }
568    }
569
570    private final class HdmiDeviceEventListener extends IHdmiDeviceEventListener.Stub {
571        @Override
572        public void onStatusChanged(HdmiCecDeviceInfo deviceInfo, boolean activated) {
573            mHandler.obtainMessage(
574                    activated ? ListenerHandler.CEC_DEVICE_ADDED
575                    : ListenerHandler.CEC_DEVICE_REMOVED,
576                    0, 0, deviceInfo).sendToTarget();
577        }
578    }
579
580    private final class HdmiInputChangeListener extends IHdmiInputChangeListener.Stub {
581        @Override
582        public void onChanged(HdmiCecDeviceInfo device) throws RemoteException {
583            // TODO: Build a channel Uri for the TvInputInfo associated with the logical device
584            //       and send an intent to TV app
585        }
586    }
587}
588