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