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