TvInputHardwareManager.java revision 839ae5f460caadf8580b7e0ab77e255d7a1ddae5
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 android.content.Context;
20import android.media.AudioDevicePort;
21import android.media.AudioManager;
22import android.media.AudioPatch;
23import android.media.AudioPort;
24import android.media.AudioPortConfig;
25import android.media.tv.ITvInputHardware;
26import android.media.tv.ITvInputHardwareCallback;
27import android.media.tv.TvInputHardwareInfo;
28import android.media.tv.TvStreamConfig;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.util.Slog;
32import android.util.SparseArray;
33import android.view.KeyEvent;
34import android.view.Surface;
35
36import java.util.ArrayList;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Set;
40
41/**
42 * A helper class for TvInputManagerService to handle TV input hardware.
43 *
44 * This class does a basic connection management and forwarding calls to TvInputHal which eventually
45 * calls to tv_input HAL module.
46 *
47 * @hide
48 */
49class TvInputHardwareManager implements TvInputHal.Callback {
50    private static final String TAG = TvInputHardwareManager.class.getSimpleName();
51    private final TvInputHal mHal = new TvInputHal(this);
52    private final SparseArray<Connection> mConnections = new SparseArray<Connection>();
53    private final List<TvInputHardwareInfo> mInfoList = new ArrayList<TvInputHardwareInfo>();
54    private final Context mContext;
55    private final Set<Integer> mActiveHdmiSources = new HashSet<Integer>();
56    private final AudioManager mAudioManager;
57
58    private final Object mLock = new Object();
59
60    public TvInputHardwareManager(Context context) {
61        mContext = context;
62        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
63        // TODO(hdmi): mHdmiManager = mContext.getSystemService(...);
64        // TODO(hdmi): mHdmiClient = mHdmiManager.getTvClient();
65        mHal.init();
66    }
67
68    @Override
69    public void onDeviceAvailable(
70            TvInputHardwareInfo info, TvStreamConfig[] configs) {
71        synchronized (mLock) {
72            Connection connection = new Connection(info);
73            connection.updateConfigsLocked(configs);
74            mConnections.put(info.getDeviceId(), connection);
75            buildInfoListLocked();
76            // TODO: notify if necessary
77        }
78    }
79
80    private void buildInfoListLocked() {
81        mInfoList.clear();
82        for (int i = 0; i < mConnections.size(); ++i) {
83            mInfoList.add(mConnections.valueAt(i).getInfoLocked());
84        }
85    }
86
87    @Override
88    public void onDeviceUnavailable(int deviceId) {
89        synchronized (mLock) {
90            Connection connection = mConnections.get(deviceId);
91            if (connection == null) {
92                Slog.e(TAG, "onDeviceUnavailable: Cannot find a connection with " + deviceId);
93                return;
94            }
95            connection.resetLocked(null, null, null, null);
96            mConnections.remove(deviceId);
97            buildInfoListLocked();
98            // TODO: notify if necessary
99        }
100    }
101
102    @Override
103    public void onStreamConfigurationChanged(int deviceId, TvStreamConfig[] configs) {
104        synchronized (mLock) {
105            Connection connection = mConnections.get(deviceId);
106            if (connection == null) {
107                Slog.e(TAG, "StreamConfigurationChanged: Cannot find a connection with "
108                        + deviceId);
109                return;
110            }
111            connection.updateConfigsLocked(configs);
112            try {
113                connection.getCallbackLocked().onStreamConfigChanged(configs);
114            } catch (RemoteException e) {
115                Slog.e(TAG, "onStreamConfigurationChanged: " + e);
116            }
117        }
118    }
119
120    public List<TvInputHardwareInfo> getHardwareList() {
121        synchronized (mLock) {
122            return mInfoList;
123        }
124    }
125
126    private boolean checkUidChangedLocked(
127            Connection connection, int callingUid, int resolvedUserId) {
128        Integer connectionCallingUid = connection.getCallingUidLocked();
129        Integer connectionResolvedUserId = connection.getResolvedUserIdLocked();
130        if (connectionCallingUid == null || connectionResolvedUserId == null) {
131            return true;
132        }
133        if (connectionCallingUid != callingUid || connectionResolvedUserId != resolvedUserId) {
134            return true;
135        }
136        return false;
137    }
138
139    /**
140     * Create a TvInputHardware object with a specific deviceId. One service at a time can access
141     * the object, and if more than one process attempts to create hardware with the same deviceId,
142     * the latest service will get the object and all the other hardware are released. The
143     * release is notified via ITvInputHardwareCallback.onReleased().
144     */
145    public ITvInputHardware acquireHardware(int deviceId, ITvInputHardwareCallback callback,
146            int callingUid, int resolvedUserId) {
147        if (callback == null) {
148            throw new NullPointerException();
149        }
150        synchronized (mLock) {
151            Connection connection = mConnections.get(deviceId);
152            if (connection == null) {
153                Slog.e(TAG, "Invalid deviceId : " + deviceId);
154                return null;
155            }
156            if (checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
157                TvInputHardwareImpl hardware = new TvInputHardwareImpl(connection.getInfoLocked());
158                try {
159                    callback.asBinder().linkToDeath(connection, 0);
160                } catch (RemoteException e) {
161                    hardware.release();
162                    return null;
163                }
164                connection.resetLocked(hardware, callback, callingUid, resolvedUserId);
165            }
166            return connection.getHardwareLocked();
167        }
168    }
169
170    /**
171     * Release the specified hardware.
172     */
173    public void releaseHardware(int deviceId, ITvInputHardware hardware, int callingUid,
174            int resolvedUserId) {
175        synchronized (mLock) {
176            Connection connection = mConnections.get(deviceId);
177            if (connection == null) {
178                Slog.e(TAG, "Invalid deviceId : " + deviceId);
179                return;
180            }
181            if (connection.getHardwareLocked() != hardware
182                    || checkUidChangedLocked(connection, callingUid, resolvedUserId)) {
183                return;
184            }
185            connection.resetLocked(null, null, null, null);
186        }
187    }
188
189    private class Connection implements IBinder.DeathRecipient {
190        private final TvInputHardwareInfo mInfo;
191        private TvInputHardwareImpl mHardware = null;
192        private ITvInputHardwareCallback mCallback;
193        private TvStreamConfig[] mConfigs = null;
194        private Integer mCallingUid = null;
195        private Integer mResolvedUserId = null;
196
197        public Connection(TvInputHardwareInfo info) {
198            mInfo = info;
199        }
200
201        // *Locked methods assume TvInputHardwareManager.mLock is held.
202
203        public void resetLocked(TvInputHardwareImpl hardware,
204                ITvInputHardwareCallback callback, Integer callingUid, Integer resolvedUserId) {
205            if (mHardware != null) {
206                try {
207                    mCallback.onReleased();
208                } catch (RemoteException e) {
209                    Slog.e(TAG, "Connection::resetHardware: " + e);
210                }
211                mHardware.release();
212            }
213            mHardware = hardware;
214            mCallback = callback;
215            mCallingUid = callingUid;
216            mResolvedUserId = resolvedUserId;
217
218            if (mHardware != null && mCallback != null) {
219                try {
220                    mCallback.onStreamConfigChanged(getConfigsLocked());
221                } catch (RemoteException e) {
222                    Slog.e(TAG, "Connection::resetHardware: " + e);
223                }
224            }
225        }
226
227        public void updateConfigsLocked(TvStreamConfig[] configs) {
228            mConfigs = configs;
229        }
230
231        public TvInputHardwareInfo getInfoLocked() {
232            return mInfo;
233        }
234
235        public ITvInputHardware getHardwareLocked() {
236            return mHardware;
237        }
238
239        public ITvInputHardwareCallback getCallbackLocked() {
240            return mCallback;
241        }
242
243        public TvStreamConfig[] getConfigsLocked() {
244            return mConfigs;
245        }
246
247        public Integer getCallingUidLocked() {
248            return mCallingUid;
249        }
250
251        public Integer getResolvedUserIdLocked() {
252            return mResolvedUserId;
253        }
254
255        @Override
256        public void binderDied() {
257            synchronized (mLock) {
258                resetLocked(null, null, null, null);
259            }
260        }
261    }
262
263    private class TvInputHardwareImpl extends ITvInputHardware.Stub {
264        private final TvInputHardwareInfo mInfo;
265        private boolean mReleased = false;
266        private final Object mImplLock = new Object();
267
268        private final AudioDevicePort mAudioSource;
269        private final AudioDevicePort mAudioSink;
270        private AudioPatch mAudioPatch = null;
271
272        private TvStreamConfig mActiveConfig = null;
273
274        public TvInputHardwareImpl(TvInputHardwareInfo info) {
275            mInfo = info;
276            AudioDevicePort audioSource = null;
277            AudioDevicePort audioSink = null;
278            if (mInfo.getAudioType() != AudioManager.DEVICE_NONE) {
279                ArrayList<AudioPort> devicePorts = new ArrayList<AudioPort>();
280                if (mAudioManager.listAudioDevicePorts(devicePorts) == AudioManager.SUCCESS) {
281                    // Find source
282                    for (AudioPort port : devicePorts) {
283                        AudioDevicePort devicePort = (AudioDevicePort) port;
284                        if (devicePort.type() == mInfo.getAudioType() &&
285                                devicePort.address().equals(mInfo.getAudioAddress())) {
286                            audioSource = devicePort;
287                            break;
288                        }
289                    }
290                    // Find sink
291                    // TODO: App may want to specify sink device?
292                    int sinkDevices = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
293                    for (AudioPort port : devicePorts) {
294                        AudioDevicePort devicePort = (AudioDevicePort) port;
295                        if (devicePort.type() == sinkDevices) {
296                            audioSink = devicePort;
297                            break;
298                        }
299                    }
300                }
301            }
302            mAudioSource = audioSource;
303            mAudioSink = audioSink;
304        }
305
306        public void release() {
307            synchronized (mImplLock) {
308                if (mAudioPatch != null) {
309                    mAudioManager.releaseAudioPatch(mAudioPatch);
310                    mAudioPatch = null;
311                }
312                mReleased = true;
313            }
314        }
315
316        // A TvInputHardwareImpl object holds only one active session. Therefore, if a client
317        // attempts to call setSurface with different TvStreamConfig objects, the last call will
318        // prevail.
319        @Override
320        public boolean setSurface(Surface surface, TvStreamConfig config)
321                throws RemoteException {
322            synchronized (mImplLock) {
323                if (mReleased) {
324                    throw new IllegalStateException("Device already released.");
325                }
326                if (surface != null && config == null) {
327                    return false;
328                }
329                if (surface == null && mActiveConfig == null) {
330                    return false;
331                }
332                if (mInfo.getType() == TvInputHal.TYPE_HDMI) {
333                    if (surface != null) {
334                        // Set "Active Source" for HDMI.
335                        // TODO(hdmi): mHdmiClient.deviceSelect(...);
336                        mActiveHdmiSources.add(mInfo.getDeviceId());
337                    } else {
338                        mActiveHdmiSources.remove(mInfo.getDeviceId());
339                        if (mActiveHdmiSources.size() == 0) {
340                            // Tell HDMI that no HDMI source is active
341                            // TODO(hdmi): mHdmiClient.portSelect(null);
342                        }
343                    }
344                }
345                if (mAudioSource != null && mAudioSink != null) {
346                    if (surface != null) {
347                        AudioPortConfig sourceConfig = mAudioSource.activeConfig();
348                        AudioPortConfig sinkConfig = mAudioSink.activeConfig();
349                        AudioPatch[] audioPatchArray = new AudioPatch[] { mAudioPatch };
350                        // TODO: build config if activeConfig() == null
351                        mAudioManager.createAudioPatch(
352                                audioPatchArray,
353                                new AudioPortConfig[] { sourceConfig },
354                                new AudioPortConfig[] { sinkConfig });
355                        mAudioPatch = audioPatchArray[0];
356                    } else {
357                        mAudioManager.releaseAudioPatch(mAudioPatch);
358                        mAudioPatch = null;
359                    }
360                }
361                int result = TvInputHal.ERROR_UNKNOWN;
362                if (surface == null) {
363                    result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
364                    mActiveConfig = null;
365                } else {
366                    if (config != mActiveConfig && mActiveConfig != null) {
367                        result = mHal.removeStream(mInfo.getDeviceId(), mActiveConfig);
368                        if (result != TvInputHal.SUCCESS) {
369                            mActiveConfig = null;
370                            return false;
371                        }
372                    }
373                    result = mHal.addStream(mInfo.getDeviceId(), surface, config);
374                    if (result == TvInputHal.SUCCESS) {
375                        mActiveConfig = config;
376                    }
377                }
378                return result == TvInputHal.SUCCESS;
379            }
380        }
381
382        @Override
383        public void setVolume(float volume) throws RemoteException {
384            synchronized (mImplLock) {
385                if (mReleased) {
386                    throw new IllegalStateException("Device already released.");
387                }
388            }
389            // TODO: Use AudioGain?
390        }
391
392        @Override
393        public boolean dispatchKeyEventToHdmi(KeyEvent event) throws RemoteException {
394            synchronized (mImplLock) {
395                if (mReleased) {
396                    throw new IllegalStateException("Device already released.");
397                }
398            }
399            if (mInfo.getType() != TvInputHal.TYPE_HDMI) {
400                return false;
401            }
402            // TODO(hdmi): mHdmiClient.sendKeyEvent(event);
403            return false;
404        }
405    }
406}
407