| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.tuner.source; |
| |
| import android.content.Context; |
| |
| import com.android.tv.common.AutoCloseableUtils; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.tuner.TunerHal; |
| import com.android.tv.tuner.data.TunerChannel; |
| import com.android.tv.tuner.tvinput.EventDetector; |
| |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Manages {@link TunerTsStreamer} for playback and recording. |
| * The class hides handling of {@link TunerHal} from other classes. |
| * This class is used by {@link TsDataSourceManager}. Don't use this class directly. |
| */ |
| class TunerTsStreamerManager { |
| // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator |
| // to support timely {@link TunerTsStreamer} cancellation due to a new tune request from |
| // the same session. |
| private final Object mCancelLock = new Object(); |
| private final StreamerFinder mStreamerFinder = new StreamerFinder(); |
| private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>(); |
| private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>(); |
| private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>(); |
| private final TunerHalManager mTunerHalManager = new TunerHalManager(); |
| private static TunerTsStreamerManager sInstance; |
| |
| /** |
| * Returns the singleton instance for the class |
| * @return TunerTsStreamerManager |
| */ |
| static synchronized TunerTsStreamerManager getInstance() { |
| if (sInstance == null) { |
| sInstance = new TunerTsStreamerManager(); |
| } |
| return sInstance; |
| } |
| |
| private TunerTsStreamerManager() { } |
| |
| synchronized TsDataSource createDataSource( |
| Context context, TunerChannel channel, EventDetector.EventListener listener, |
| int sessionId, boolean reuse) { |
| TsStreamerCreator creator; |
| synchronized (mCancelLock) { |
| if (mStreamerFinder.containsLocked(channel)) { |
| mStreamerFinder.appendSessionLocked(channel, sessionId); |
| TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel); |
| TsDataSource source = streamer.createDataSource(); |
| mListeners.put(sessionId, listener); |
| streamer.registerListener(listener); |
| mSourceToStreamerMap.put(source, streamer); |
| return source; |
| } |
| creator = new TsStreamerCreator(context, channel, listener); |
| mCreators.put(sessionId, creator); |
| } |
| TunerTsStreamer streamer = creator.create(sessionId, reuse); |
| synchronized (mCancelLock) { |
| mCreators.remove(sessionId); |
| if (streamer == null) { |
| return null; |
| } |
| if (!creator.isCancelledLocked()) { |
| mStreamerFinder.putLocked(channel, sessionId, streamer); |
| TsDataSource source = streamer.createDataSource(); |
| mListeners.put(sessionId, listener); |
| streamer.registerListener(listener); |
| mSourceToStreamerMap.put(source, streamer); |
| return source; |
| } |
| } |
| // Created streamer was cancelled by a new tune request. |
| streamer.stopStream(); |
| TunerHal hal = streamer.getTunerHal(); |
| hal.setHasPendingTune(false); |
| mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); |
| return null; |
| } |
| |
| synchronized void releaseDataSource(TsDataSource source, int sessionId, |
| boolean reuse) { |
| TunerTsStreamer streamer; |
| synchronized (mCancelLock) { |
| streamer = mSourceToStreamerMap.get(source); |
| mSourceToStreamerMap.remove(source); |
| if (streamer == null) { |
| return; |
| } |
| EventDetector.EventListener listener = mListeners.remove(sessionId); |
| streamer.unregisterListener(listener); |
| TunerChannel channel = streamer.getChannel(); |
| SoftPreconditions.checkState(channel != null); |
| mStreamerFinder.removeSessionLocked(channel, sessionId); |
| if (mStreamerFinder.containsLocked(channel)) { |
| return; |
| } |
| } |
| streamer.stopStream(); |
| TunerHal hal = streamer.getTunerHal(); |
| hal.setHasPendingTune(false); |
| mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); |
| } |
| |
| void setHasPendingTune(int sessionId) { |
| synchronized (mCancelLock) { |
| if (mCreators.containsKey(sessionId)) { |
| mCreators.get(sessionId).cancelLocked(); |
| } |
| } |
| } |
| |
| /** |
| * Add tuner hal into TunerHalManager for test. |
| */ |
| void addTunerHal(TunerHal tunerHal, int sessionId) { |
| mTunerHalManager.addTunerHal(tunerHal, sessionId); |
| } |
| |
| synchronized void release(int sessionId) { |
| mTunerHalManager.releaseCachedHal(sessionId); |
| } |
| |
| private class StreamerFinder { |
| private final Map<TunerChannel, Set<Integer>> mSessions = new HashMap<>(); |
| private final Map<TunerChannel, TunerTsStreamer> mStreamers = new HashMap<>(); |
| |
| // @GuardedBy("mCancelLock") |
| private void putLocked(TunerChannel channel, int sessionId, TunerTsStreamer streamer) { |
| Set<Integer> sessions = new HashSet<>(); |
| sessions.add(sessionId); |
| mSessions.put(channel, sessions); |
| mStreamers.put(channel, streamer); |
| } |
| |
| // @GuardedBy("mCancelLock") |
| private void appendSessionLocked(TunerChannel channel, int sessionId) { |
| if (mSessions.containsKey(channel)) { |
| mSessions.get(channel).add(sessionId); |
| } |
| } |
| |
| // @GuardedBy("mCancelLock") |
| private void removeSessionLocked(TunerChannel channel, int sessionId) { |
| Set<Integer> sessions = mSessions.get(channel); |
| sessions.remove(sessionId); |
| if (sessions.size() == 0) { |
| mSessions.remove(channel); |
| mStreamers.remove(channel); |
| } |
| } |
| |
| // @GuardedBy("mCancelLock") |
| private boolean containsLocked(TunerChannel channel) { |
| return mSessions.containsKey(channel); |
| } |
| |
| // @GuardedBy("mCancelLock") |
| private TunerTsStreamer getStreamerLocked(TunerChannel channel) { |
| return mStreamers.containsKey(channel) ? mStreamers.get(channel) : null; |
| } |
| } |
| |
| /** |
| * {@link TunerTsStreamer} creation can be cancelled by a new tune request for the same |
| * session. The class supports the cancellation in creating new {@link TunerTsStreamer}. |
| */ |
| private class TsStreamerCreator { |
| private final Context mContext; |
| private final TunerChannel mChannel; |
| private final EventDetector.EventListener mEventListener; |
| // mCancelled will be {@code true} if a new tune request for the same session |
| // cancels create(). |
| private boolean mCancelled; |
| private TunerHal mTunerHal; |
| |
| private TsStreamerCreator(Context context, TunerChannel channel, |
| EventDetector.EventListener listener) { |
| mContext = context; |
| mChannel = channel; |
| mEventListener = listener; |
| } |
| |
| private TunerTsStreamer create(int sessionId, boolean reuse) { |
| TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId); |
| if (hal == null) { |
| return null; |
| } |
| boolean canceled = false; |
| synchronized (mCancelLock) { |
| if (!mCancelled) { |
| mTunerHal = hal; |
| } else { |
| canceled = true; |
| } |
| } |
| if (!canceled) { |
| TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext); |
| if (tsStreamer.startStream(mChannel)) { |
| return tsStreamer; |
| } |
| synchronized (mCancelLock) { |
| mTunerHal = null; |
| } |
| } |
| hal.setHasPendingTune(false); |
| // Since TunerTsStreamer is not properly created, closes TunerHal. |
| // And do not re-use TunerHal when it is not cancelled. |
| mTunerHalManager.releaseTunerHal(hal, sessionId, mCancelled && reuse); |
| return null; |
| } |
| |
| // @GuardedBy("mCancelLock") |
| private void cancelLocked() { |
| if (mCancelled) { |
| return; |
| } |
| mCancelled = true; |
| if (mTunerHal != null) { |
| mTunerHal.setHasPendingTune(true); |
| } |
| } |
| |
| // @GuardedBy("mCancelLock") |
| private boolean isCancelledLocked() { |
| return mCancelled; |
| } |
| } |
| |
| /** |
| * Supports sharing {@link TunerHal} among multiple sessions. |
| * The class also supports session affinity for {@link TunerHal} allocation. |
| */ |
| private class TunerHalManager { |
| private final Map<Integer, TunerHal> mTunerHals = new HashMap<>(); |
| |
| private TunerHal getOrCreateTunerHal(Context context, int sessionId) { |
| // Handles session affinity. |
| TunerHal hal = mTunerHals.get(sessionId); |
| if (hal != null) { |
| mTunerHals.remove(sessionId); |
| return hal; |
| } |
| // Finds a TunerHal which is cached for other sessions. |
| Iterator it = mTunerHals.keySet().iterator(); |
| if (it.hasNext()) { |
| Integer key = (Integer) it.next(); |
| hal = mTunerHals.get(key); |
| mTunerHals.remove(key); |
| return hal; |
| } |
| return TunerHal.createInstance(context); |
| } |
| |
| private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) { |
| if (!reuse || !hal.isReusable()) { |
| AutoCloseableUtils.closeQuietly(hal); |
| return; |
| } |
| TunerHal cachedHal = mTunerHals.get(sessionId); |
| if (cachedHal != hal) { |
| mTunerHals.put(sessionId, hal); |
| if (cachedHal != null) { |
| AutoCloseableUtils.closeQuietly(cachedHal); |
| } |
| } |
| } |
| |
| private void releaseCachedHal(int sessionId) { |
| TunerHal hal = mTunerHals.get(sessionId); |
| if (hal != null) { |
| mTunerHals.remove(sessionId); |
| } |
| if (hal != null) { |
| AutoCloseableUtils.closeQuietly(hal); |
| } |
| } |
| |
| private void addTunerHal(TunerHal tunerHal, int sessionId) { |
| mTunerHals.put(sessionId, tunerHal); |
| } |
| } |
| } |