1/*
2 * Copyright (C) 2016 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.tv.tuner.source;
18
19import android.content.Context;
20
21import com.android.tv.common.AutoCloseableUtils;
22import com.android.tv.common.SoftPreconditions;
23import com.android.tv.tuner.TunerHal;
24import com.android.tv.tuner.data.TunerChannel;
25import com.android.tv.tuner.tvinput.EventDetector;
26
27import java.util.HashMap;
28import java.util.HashSet;
29import java.util.Iterator;
30import java.util.Map;
31import java.util.Set;
32
33/**
34 * Manages {@link TunerTsStreamer} for playback and recording.
35 * The class hides handling of {@link TunerHal} from other classes.
36 * This class is used by {@link TsDataSourceManager}. Don't use this class directly.
37 */
38class TunerTsStreamerManager {
39    // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator
40    // to support timely {@link TunerTsStreamer} cancellation due to a new tune request from
41    // the same session.
42    private final Object mCancelLock = new Object();
43    private final StreamerFinder mStreamerFinder = new StreamerFinder();
44    private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>();
45    private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
46    private final TunerHalManager mTunerHalManager = new TunerHalManager();
47    private static TunerTsStreamerManager sInstance;
48
49    /**
50     * Returns the singleton instance for the class
51     * @return TunerTsStreamerManager
52     */
53    static synchronized TunerTsStreamerManager getInstance() {
54        if (sInstance == null) {
55            sInstance = new TunerTsStreamerManager();
56        }
57        return sInstance;
58    }
59
60    private TunerTsStreamerManager() { }
61
62    synchronized TsDataSource createDataSource(
63            Context context, TunerChannel channel, EventDetector.EventListener listener,
64            int sessionId, boolean reuse) {
65        TsStreamerCreator creator;
66        synchronized (mCancelLock) {
67            if (mStreamerFinder.containsLocked(channel)) {
68                mStreamerFinder.appendSessionLocked(channel, sessionId);
69                TunerTsStreamer streamer =  mStreamerFinder.getStreamerLocked(channel);
70                TsDataSource source = streamer.createDataSource();
71                mSourceToStreamerMap.put(source, streamer);
72                return source;
73            }
74            creator = new TsStreamerCreator(context, channel, listener);
75            mCreators.put(sessionId, creator);
76        }
77        TunerTsStreamer streamer = creator.create(sessionId, reuse);
78        synchronized (mCancelLock) {
79            mCreators.remove(sessionId);
80            if (streamer == null) {
81                return null;
82            }
83            if (!creator.isCancelledLocked()) {
84                mStreamerFinder.putLocked(channel, sessionId, streamer);
85                TsDataSource source = streamer.createDataSource();
86                mSourceToStreamerMap.put(source, streamer);
87                return source;
88            }
89        }
90        // Created streamer was cancelled by a new tune request.
91        streamer.stopStream();
92        TunerHal hal = streamer.getTunerHal();
93        hal.setHasPendingTune(false);
94        mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
95        return null;
96    }
97
98    synchronized void releaseDataSource(TsDataSource source, int sessionId,
99            boolean reuse) {
100        TunerTsStreamer streamer;
101        synchronized (mCancelLock) {
102            streamer = mSourceToStreamerMap.get(source);
103            mSourceToStreamerMap.remove(source);
104            if (streamer == null) {
105                return;
106            }
107            TunerChannel channel = streamer.getChannel();
108            SoftPreconditions.checkState(channel != null);
109            mStreamerFinder.removeSessionLocked(channel, sessionId);
110            if (mStreamerFinder.containsLocked(channel)) {
111                return;
112            }
113        }
114        streamer.stopStream();
115        TunerHal hal = streamer.getTunerHal();
116        hal.setHasPendingTune(false);
117        mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
118    }
119
120    void setHasPendingTune(int sessionId) {
121        synchronized (mCancelLock) {
122           if (mCreators.containsKey(sessionId)) {
123               mCreators.get(sessionId).cancelLocked();
124           }
125        }
126    }
127
128    synchronized void release(int sessionId) {
129        mTunerHalManager.releaseCachedHal(sessionId);
130    }
131
132    private class StreamerFinder {
133        private final Map<TunerChannel, Set<Integer>> mSessions = new HashMap<>();
134        private final Map<TunerChannel, TunerTsStreamer> mStreamers = new HashMap<>();
135
136        // @GuardedBy("mCancelLock")
137        private void putLocked(TunerChannel channel, int sessionId, TunerTsStreamer streamer) {
138            Set<Integer> sessions = new HashSet<>();
139            sessions.add(sessionId);
140            mSessions.put(channel, sessions);
141            mStreamers.put(channel, streamer);
142        }
143
144        // @GuardedBy("mCancelLock")
145        private void appendSessionLocked(TunerChannel channel, int sessionId) {
146            if (mSessions.containsKey(channel)) {
147                mSessions.get(channel).add(sessionId);
148            }
149        }
150
151        // @GuardedBy("mCancelLock")
152        private void removeSessionLocked(TunerChannel channel, int sessionId) {
153            Set<Integer> sessions = mSessions.get(channel);
154            sessions.remove(sessionId);
155            if (sessions.size() == 0) {
156                mSessions.remove(channel);
157                mStreamers.remove(channel);
158            }
159        }
160
161        // @GuardedBy("mCancelLock")
162        private boolean containsLocked(TunerChannel channel) {
163            return mSessions.containsKey(channel);
164        }
165
166        // @GuardedBy("mCancelLock")
167        private TunerTsStreamer getStreamerLocked(TunerChannel channel) {
168            return mStreamers.containsKey(channel) ? mStreamers.get(channel) : null;
169        }
170    }
171
172    /**
173     * {@link TunerTsStreamer} creation can be cancelled by a new tune request for the same
174     * session. The class supports the cancellation in creating new {@link TunerTsStreamer}.
175     */
176    private class TsStreamerCreator {
177        private final Context mContext;
178        private final TunerChannel mChannel;
179        private final EventDetector.EventListener mEventListener;
180        // mCancelled will be {@code true} if a new tune request for the same session
181        // cancels create().
182        private boolean mCancelled;
183        private TunerHal mTunerHal;
184
185        private TsStreamerCreator(Context context, TunerChannel channel,
186                EventDetector.EventListener listener) {
187            mContext = context;
188            mChannel = channel;
189            mEventListener = listener;
190        }
191
192        private TunerTsStreamer create(int sessionId, boolean reuse) {
193            TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId);
194            if (hal == null) {
195                return null;
196            }
197            boolean canceled = false;
198            synchronized (mCancelLock) {
199                if (!mCancelled) {
200                    mTunerHal = hal;
201                } else {
202                    canceled = true;
203                }
204            }
205            if (!canceled) {
206                TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext);
207                if (tsStreamer.startStream(mChannel)) {
208                    return tsStreamer;
209                }
210                synchronized (mCancelLock) {
211                    mTunerHal = null;
212                }
213            }
214            hal.setHasPendingTune(false);
215            // Since TunerTsStreamer is not properly created, closes TunerHal.
216            // And do not re-use TunerHal when it is not cancelled.
217            mTunerHalManager.releaseTunerHal(hal, sessionId, mCancelled && reuse);
218            return null;
219        }
220
221        // @GuardedBy("mCancelLock")
222        private void cancelLocked() {
223                if (mCancelled) {
224                    return;
225                }
226                mCancelled = true;
227                if (mTunerHal != null) {
228                    mTunerHal.setHasPendingTune(true);
229                }
230        }
231
232        // @GuardedBy("mCancelLock")
233        private boolean isCancelledLocked() {
234                return mCancelled;
235        }
236    }
237
238    /**
239     * Supports sharing {@link TunerHal} among multiple sessions.
240     * The class also supports session affinity for {@link TunerHal} allocation.
241     */
242    private class TunerHalManager {
243        private final Map<Integer, TunerHal> mTunerHals = new HashMap<>();
244
245        private TunerHal getOrCreateTunerHal(Context context, int sessionId) {
246            // Handles session affinity.
247            TunerHal hal = mTunerHals.get(sessionId);
248            if (hal != null) {
249                mTunerHals.remove(sessionId);
250                return hal;
251            }
252            // Finds a TunerHal which is cached for other sessions.
253            Iterator it = mTunerHals.keySet().iterator();
254            if (it.hasNext()) {
255                Integer key = (Integer) it.next();
256                hal = mTunerHals.get(key);
257                mTunerHals.remove(key);
258                return hal;
259            }
260            return TunerHal.createInstance(context);
261        }
262
263        private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) {
264            if (!reuse) {
265                AutoCloseableUtils.closeQuietly(hal);
266                return;
267            }
268            TunerHal cachedHal = mTunerHals.get(sessionId);
269            if (cachedHal != hal) {
270                mTunerHals.put(sessionId, hal);
271            }
272            if (cachedHal != null && cachedHal != hal) {
273                AutoCloseableUtils.closeQuietly(cachedHal);
274            }
275        }
276
277        private void releaseCachedHal(int sessionId) {
278            TunerHal hal = mTunerHals.get(sessionId);
279            if (hal != null) {
280                mTunerHals.remove(sessionId);
281            }
282            if (hal != null) {
283                AutoCloseableUtils.closeQuietly(hal);
284            }
285        }
286    }
287}