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