1 /* 2 * Copyright (C) 2015 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 17 package com.android.tv; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.media.tv.TvContentRating; 22 import android.media.tv.TvInputInfo; 23 import android.media.tv.TvRecordingClient; 24 import android.media.tv.TvRecordingClient.RecordingCallback; 25 import android.media.tv.TvTrackInfo; 26 import android.media.tv.TvView; 27 import android.media.tv.TvView.TvInputCallback; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.support.annotation.MainThread; 34 import android.support.annotation.NonNull; 35 import android.support.annotation.Nullable; 36 import android.text.TextUtils; 37 import android.util.ArraySet; 38 import android.util.Log; 39 40 import com.android.tv.common.SoftPreconditions; 41 import com.android.tv.data.Channel; 42 import com.android.tv.ui.TunableTvView; 43 import com.android.tv.ui.TunableTvView.OnTuneListener; 44 import com.android.tv.util.TvInputManagerHelper; 45 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.Set; 50 51 /** 52 * Manages input sessions. 53 * Responsible for: 54 * <ul> 55 * <li>Manage {@link TvView} sessions and recording sessions</li> 56 * <li>Manage capabilities (conflict)</li> 57 * </ul> 58 * <p> 59 * As TvView's methods should be called on the main thread and the {@link RecordingSession} should 60 * look at the state of the {@link TvViewSession} when it calls the framework methods, the framework 61 * calls in RecordingSession are made on the main thread not to introduce the multi-thread problems. 62 */ 63 @TargetApi(Build.VERSION_CODES.N) 64 public class InputSessionManager { 65 private static final String TAG = "InputSessionManager"; 66 private static final boolean DEBUG = false; 67 68 private final Context mContext; 69 private final TvInputManagerHelper mInputManager; 70 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 71 private final Set<TvViewSession> mTvViewSessions = new ArraySet<>(); 72 private final Set<RecordingSession> mRecordingSessions = 73 Collections.synchronizedSet(new ArraySet<>()); 74 private final Set<OnTvViewChannelChangeListener> mOnTvViewChannelChangeListeners = 75 new ArraySet<>(); 76 InputSessionManager(Context context)77 public InputSessionManager(Context context) { 78 mContext = context.getApplicationContext(); 79 mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper(); 80 } 81 82 /** 83 * Creates the session for {@link TvView}. 84 * <p> 85 * Do not call {@link TvView#setCallback} after the session is created. 86 */ 87 @MainThread 88 @NonNull createTvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback)89 public TvViewSession createTvViewSession(TvView tvView, TunableTvView tunableTvView, 90 TvInputCallback callback) { 91 TvViewSession session = new TvViewSession(tvView, tunableTvView, callback); 92 mTvViewSessions.add(session); 93 if (DEBUG) Log.d(TAG, "TvView session created: " + session); 94 return session; 95 } 96 97 /** 98 * Releases the {@link TvView} session. 99 */ 100 @MainThread releaseTvViewSession(TvViewSession session)101 public void releaseTvViewSession(TvViewSession session) { 102 mTvViewSessions.remove(session); 103 session.reset(); 104 if (DEBUG) Log.d(TAG, "TvView session released: " + session); 105 } 106 107 /** 108 * Creates the session for recording. 109 */ 110 @NonNull createRecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler, long endTimeMs)111 public RecordingSession createRecordingSession(String inputId, String tag, 112 RecordingCallback callback, Handler handler, long endTimeMs) { 113 RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); 114 mRecordingSessions.add(session); 115 if (DEBUG) Log.d(TAG, "Recording session created: " + session); 116 return session; 117 } 118 119 /** 120 * Releases the recording session. 121 */ releaseRecordingSession(RecordingSession session)122 public void releaseRecordingSession(RecordingSession session) { 123 mRecordingSessions.remove(session); 124 session.release(); 125 if (DEBUG) Log.d(TAG, "Recording session released: " + session); 126 } 127 128 /** 129 * Adds the {@link OnTvViewChannelChangeListener}. 130 */ 131 @MainThread addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener)132 public void addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) { 133 mOnTvViewChannelChangeListeners.add(listener); 134 } 135 136 /** 137 * Removes the {@link OnTvViewChannelChangeListener}. 138 */ 139 @MainThread removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener)140 public void removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) { 141 mOnTvViewChannelChangeListeners.remove(listener); 142 } 143 144 @MainThread notifyTvViewChannelChange(Uri channelUri)145 void notifyTvViewChannelChange(Uri channelUri) { 146 for (OnTvViewChannelChangeListener l : mOnTvViewChannelChangeListeners) { 147 l.onTvViewChannelChange(channelUri); 148 } 149 } 150 151 /** 152 * Returns the current {@link TvView} channel. 153 */ 154 @MainThread getCurrentTvViewChannelUri()155 public Uri getCurrentTvViewChannelUri() { 156 for (TvViewSession session : mTvViewSessions) { 157 if (session.mTuned) { 158 return session.mChannelUri; 159 } 160 } 161 return null; 162 } 163 164 /** 165 * Retruns the earliest end time of recording sessions in progress of the certain TV input. 166 */ 167 @MainThread getEarliestRecordingSessionEndTimeMs(String inputId)168 public Long getEarliestRecordingSessionEndTimeMs(String inputId) { 169 long timeMs = Long.MAX_VALUE; 170 synchronized (mRecordingSessions) { 171 for (RecordingSession session : mRecordingSessions) { 172 if (session.mTuned && TextUtils.equals(inputId, session.mInputId)) { 173 if (session.mEndTimeMs < timeMs) { 174 timeMs = session.mEndTimeMs; 175 } 176 } 177 } 178 } 179 return timeMs == Long.MAX_VALUE ? null : timeMs; 180 } 181 182 @MainThread getTunedTvViewSessionCount(String inputId)183 int getTunedTvViewSessionCount(String inputId) { 184 int tunedCount = 0; 185 for (TvViewSession session : mTvViewSessions) { 186 if (session.mTuned && Objects.equals(inputId, session.mInputId)) { 187 ++tunedCount; 188 } 189 } 190 return tunedCount; 191 } 192 193 @MainThread isTunedForTvView(Uri channelUri)194 boolean isTunedForTvView(Uri channelUri) { 195 for (TvViewSession session : mTvViewSessions) { 196 if (session.mTuned && Objects.equals(channelUri, session.mChannelUri)) { 197 return true; 198 } 199 } 200 return false; 201 } 202 getTunedRecordingSessionCount(String inputId)203 int getTunedRecordingSessionCount(String inputId) { 204 synchronized (mRecordingSessions) { 205 int tunedCount = 0; 206 for (RecordingSession session : mRecordingSessions) { 207 if (session.mTuned && Objects.equals(inputId, session.mInputId)) { 208 ++tunedCount; 209 } 210 } 211 return tunedCount; 212 } 213 } 214 isTunedForRecording(Uri channelUri)215 boolean isTunedForRecording(Uri channelUri) { 216 synchronized (mRecordingSessions) { 217 for (RecordingSession session : mRecordingSessions) { 218 if (session.mTuned && Objects.equals(channelUri, session.mChannelUri)) { 219 return true; 220 } 221 } 222 return false; 223 } 224 } 225 226 /** 227 * The session for {@link TvView}. 228 * <p> 229 * The methods which create or release session for the TV input should be called through this 230 * session. 231 */ 232 @MainThread 233 public class TvViewSession { 234 private final TvView mTvView; 235 private final TunableTvView mTunableTvView; 236 private final TvInputCallback mCallback; 237 private Channel mChannel; 238 private String mInputId; 239 private Uri mChannelUri; 240 private Bundle mParams; 241 private OnTuneListener mOnTuneListener; 242 private boolean mTuned; 243 private boolean mNeedToBeRetuned; 244 TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback)245 TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) { 246 mTvView = tvView; 247 mTunableTvView = tunableTvView; 248 mCallback = callback; 249 mTvView.setCallback(new DelegateTvInputCallback(mCallback) { 250 @Override 251 public void onConnectionFailed(String inputId) { 252 if (DEBUG) Log.d(TAG, "TvViewSession: commection failed"); 253 mTuned = false; 254 mNeedToBeRetuned = false; 255 super.onConnectionFailed(inputId); 256 notifyTvViewChannelChange(null); 257 } 258 259 @Override 260 public void onDisconnected(String inputId) { 261 if (DEBUG) Log.d(TAG, "TvViewSession: disconnected"); 262 mTuned = false; 263 mNeedToBeRetuned = false; 264 super.onDisconnected(inputId); 265 notifyTvViewChannelChange(null); 266 } 267 }); 268 } 269 270 /** 271 * Tunes to the channel. 272 * <p> 273 * As this is called only for the warming up, there's no need to be retuned. 274 */ tune(String inputId, Uri channelUri)275 public void tune(String inputId, Uri channelUri) { 276 if (DEBUG) { 277 Log.d(TAG, "warm-up tune: {input=" + inputId + ", channelUri=" + channelUri + "}"); 278 } 279 mInputId = inputId; 280 mChannelUri = channelUri; 281 mTuned = true; 282 mNeedToBeRetuned = false; 283 mTvView.tune(inputId, channelUri); 284 notifyTvViewChannelChange(channelUri); 285 } 286 287 /** 288 * Tunes to the channel. 289 */ tune(Channel channel, Bundle params, OnTuneListener listener)290 public void tune(Channel channel, Bundle params, OnTuneListener listener) { 291 if (DEBUG) { 292 Log.d(TAG, "tune: {session=" + this + ", channel=" + channel + ", params=" + params 293 + ", listener=" + listener + ", mTuned=" + mTuned + "}"); 294 } 295 mChannel = channel; 296 mInputId = channel.getInputId(); 297 mChannelUri = channel.getUri(); 298 mParams = params; 299 mOnTuneListener = listener; 300 TvInputInfo input = mInputManager.getTvInputInfo(mInputId); 301 if (input == null || (input.canRecord() && !isTunedForRecording(mChannelUri) 302 && getTunedRecordingSessionCount(mInputId) >= input.getTunerCount())) { 303 if (DEBUG) { 304 if (input == null) { 305 Log.d(TAG, "Can't find input for input ID: " + mInputId); 306 } else { 307 Log.d(TAG, "No more tuners to tune for input: " + input); 308 } 309 } 310 mCallback.onConnectionFailed(mInputId); 311 // Release the previous session to not to hold the unnecessary session. 312 resetByRecording(); 313 return; 314 } 315 mTuned = true; 316 mNeedToBeRetuned = false; 317 mTvView.tune(mInputId, mChannelUri, params); 318 notifyTvViewChannelChange(mChannelUri); 319 } 320 retune()321 void retune() { 322 if (DEBUG) Log.d(TAG, "Retune requested."); 323 if (mNeedToBeRetuned) { 324 if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}"); 325 mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener); 326 mNeedToBeRetuned = false; 327 } 328 } 329 330 /** 331 * Plays a given recorded TV program. 332 * 333 * @see TvView#timeShiftPlay 334 */ timeShiftPlay(String inputId, Uri recordedProgramUri)335 public void timeShiftPlay(String inputId, Uri recordedProgramUri) { 336 mTuned = false; 337 mNeedToBeRetuned = false; 338 mTvView.timeShiftPlay(inputId, recordedProgramUri); 339 notifyTvViewChannelChange(null); 340 } 341 342 /** 343 * Resets this TvView. 344 */ reset()345 public void reset() { 346 if (DEBUG) Log.d(TAG, "Reset TvView session"); 347 mTuned = false; 348 mTvView.reset(); 349 mNeedToBeRetuned = false; 350 notifyTvViewChannelChange(null); 351 } 352 resetByRecording()353 void resetByRecording() { 354 mCallback.onVideoUnavailable(mInputId, 355 TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE); 356 if (mTuned) { 357 if (DEBUG) Log.d(TAG, "Reset TvView session by recording"); 358 mTunableTvView.resetByRecording(); 359 reset(); 360 } 361 mNeedToBeRetuned = true; 362 } 363 } 364 365 /** 366 * The session for recording. 367 * <p> 368 * The caller is responsible for releasing the session when the error occurs. 369 */ 370 public class RecordingSession { 371 private final String mInputId; 372 private Uri mChannelUri; 373 private final RecordingCallback mCallback; 374 private final Handler mHandler; 375 private volatile long mEndTimeMs; 376 private TvRecordingClient mClient; 377 private boolean mTuned; 378 RecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler, long endTimeMs)379 RecordingSession(String inputId, String tag, RecordingCallback callback, 380 Handler handler, long endTimeMs) { 381 mInputId = inputId; 382 mCallback = callback; 383 mHandler = handler; 384 mClient = new TvRecordingClient(mContext, tag, callback, handler); 385 mEndTimeMs = endTimeMs; 386 } 387 release()388 void release() { 389 if (DEBUG) Log.d(TAG, "Release of recording session requested."); 390 runOnHandler(mMainThreadHandler, new Runnable() { 391 @Override 392 public void run() { 393 if (DEBUG) Log.d(TAG, "Releasing of recording session."); 394 mTuned = false; 395 mClient.release(); 396 mClient = null; 397 for (TvViewSession session : mTvViewSessions) { 398 if (DEBUG) { 399 Log.d(TAG, "Finding TvView sessions for retune: {tuned=" 400 + session.mTuned + ", inputId=" + session.mInputId 401 + ", session=" + session + "}"); 402 } 403 if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) { 404 session.retune(); 405 break; 406 } 407 } 408 } 409 }); 410 } 411 412 /** 413 * Tunes to the channel for recording. 414 */ tune(String inputId, Uri channelUri)415 public void tune(String inputId, Uri channelUri) { 416 runOnHandler(mMainThreadHandler, new Runnable() { 417 @Override 418 public void run() { 419 int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId); 420 TvInputInfo input = mInputManager.getTvInputInfo(inputId); 421 if (input == null || !input.canRecord() 422 || input.getTunerCount() <= tunedRecordingSessionCount) { 423 runOnHandler(mHandler, new Runnable() { 424 @Override 425 public void run() { 426 mCallback.onConnectionFailed(inputId); 427 } 428 }); 429 return; 430 } 431 mTuned = true; 432 int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); 433 if (!isTunedForTvView(channelUri) && tunedTuneSessionCount > 0 434 && tunedRecordingSessionCount + tunedTuneSessionCount 435 >= input.getTunerCount()) { 436 for (TvViewSession session : mTvViewSessions) { 437 if (session.mTuned && Objects.equals(session.mInputId, inputId) 438 && !isTunedForRecording(session.mChannelUri)) { 439 session.resetByRecording(); 440 break; 441 } 442 } 443 } 444 mChannelUri = channelUri; 445 mClient.tune(inputId, channelUri); 446 } 447 }); 448 } 449 450 /** 451 * Starts recording. 452 */ startRecording(Uri programHintUri)453 public void startRecording(Uri programHintUri) { 454 mClient.startRecording(programHintUri); 455 } 456 457 /** 458 * Stops recording. 459 */ stopRecording()460 public void stopRecording() { 461 mClient.stopRecording(); 462 } 463 464 /** 465 * Sets recording session's ending time. 466 */ setEndTimeMs(long endTimeMs)467 public void setEndTimeMs(long endTimeMs) { 468 mEndTimeMs = endTimeMs; 469 } 470 runOnHandler(Handler handler, Runnable runnable)471 private void runOnHandler(Handler handler, Runnable runnable) { 472 if (Looper.myLooper() == handler.getLooper()) { 473 runnable.run(); 474 } else { 475 handler.post(runnable); 476 } 477 } 478 } 479 480 private static class DelegateTvInputCallback extends TvInputCallback { 481 private final TvInputCallback mDelegate; 482 DelegateTvInputCallback(TvInputCallback delegate)483 DelegateTvInputCallback(TvInputCallback delegate) { 484 mDelegate = delegate; 485 } 486 487 @Override onConnectionFailed(String inputId)488 public void onConnectionFailed(String inputId) { 489 mDelegate.onConnectionFailed(inputId); 490 } 491 492 @Override onDisconnected(String inputId)493 public void onDisconnected(String inputId) { 494 mDelegate.onDisconnected(inputId); 495 } 496 497 @Override onChannelRetuned(String inputId, Uri channelUri)498 public void onChannelRetuned(String inputId, Uri channelUri) { 499 mDelegate.onChannelRetuned(inputId, channelUri); 500 } 501 502 @Override onTracksChanged(String inputId, List<TvTrackInfo> tracks)503 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 504 mDelegate.onTracksChanged(inputId, tracks); 505 } 506 507 @Override onTrackSelected(String inputId, int type, String trackId)508 public void onTrackSelected(String inputId, int type, String trackId) { 509 mDelegate.onTrackSelected(inputId, type, trackId); 510 } 511 512 @Override onVideoSizeChanged(String inputId, int width, int height)513 public void onVideoSizeChanged(String inputId, int width, int height) { 514 mDelegate.onVideoSizeChanged(inputId, width, height); 515 } 516 517 @Override onVideoAvailable(String inputId)518 public void onVideoAvailable(String inputId) { 519 mDelegate.onVideoAvailable(inputId); 520 } 521 522 @Override onVideoUnavailable(String inputId, int reason)523 public void onVideoUnavailable(String inputId, int reason) { 524 mDelegate.onVideoUnavailable(inputId, reason); 525 } 526 527 @Override onContentAllowed(String inputId)528 public void onContentAllowed(String inputId) { 529 mDelegate.onContentAllowed(inputId); 530 } 531 532 @Override onContentBlocked(String inputId, TvContentRating rating)533 public void onContentBlocked(String inputId, TvContentRating rating) { 534 mDelegate.onContentBlocked(inputId, rating); 535 } 536 537 @Override onTimeShiftStatusChanged(String inputId, int status)538 public void onTimeShiftStatusChanged(String inputId, int status) { 539 mDelegate.onTimeShiftStatusChanged(inputId, status); 540 } 541 } 542 543 /** 544 * Called when the {@link TvView} channel is changed. 545 */ 546 public interface OnTvViewChannelChangeListener { onTvViewChannelChange(@ullable Uri channelUri)547 void onTvViewChannelChange(@Nullable Uri channelUri); 548 } 549 } 550