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