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