1 /* 2 * Copyright (C) 2016 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 android.media.tv; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SystemApi; 22 import android.content.Context; 23 import android.media.tv.TvInputManager; 24 import android.media.tv.interactive.TvInteractiveAppView; 25 import android.net.Uri; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.util.Pair; 32 33 import java.util.ArrayDeque; 34 import java.util.Objects; 35 import java.util.Queue; 36 37 /** 38 * The public interface object used to interact with a specific TV input service for TV program 39 * recording. 40 */ 41 public class TvRecordingClient { 42 private static final String TAG = "TvRecordingClient"; 43 private static final boolean DEBUG = false; 44 45 private final RecordingCallback mCallback; 46 private final Handler mHandler; 47 48 private final TvInputManager mTvInputManager; 49 private TvInputManager.Session mSession; 50 private MySessionCallback mSessionCallback; 51 52 private boolean mIsRecordingStarted; 53 private boolean mIsTuned; 54 private boolean mIsPaused; 55 private boolean mIsRecordingStopping; 56 private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>(); 57 private TvInteractiveAppView mTvIAppView; 58 private String mRecordingId; 59 60 /** 61 * Creates a new TvRecordingClient object. 62 * 63 * @param context The application context to create a TvRecordingClient with. 64 * @param tag A short name for debugging purposes. 65 * @param callback The callback to receive recording status changes. 66 * @param handler The handler to invoke the callback on. 67 */ TvRecordingClient(Context context, String tag, @NonNull RecordingCallback callback, Handler handler)68 public TvRecordingClient(Context context, String tag, @NonNull RecordingCallback callback, 69 Handler handler) { 70 mCallback = callback; 71 mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler; 72 mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); 73 } 74 75 /** 76 * Sets the related {@link TvInteractiveAppView} instance so the interactive app service can be 77 * notified for recording events. 78 * 79 * @param view The related {@link TvInteractiveAppView} instance that is linked to this TV 80 * recording client. {@code null} to unlink the view. 81 * @param recordingId The ID of the recording which is assigned by the TV application. 82 * {@code null} if and only if the TvInteractiveAppView parameter is 83 * {@code null}. 84 * @throws IllegalArgumentException when recording ID is {@code null} and the 85 * TvInteractiveAppView is not {@code null}; or when recording 86 * ID is not {@code null} and the TvInteractiveAppView is 87 * {@code null}. 88 * @see TvInteractiveAppView#notifyRecordingScheduled(String, String) 89 * @see TvInteractiveAppView#notifyRecordingStarted(String, String) 90 */ setTvInteractiveAppView( @ullable TvInteractiveAppView view, @Nullable String recordingId)91 public void setTvInteractiveAppView( 92 @Nullable TvInteractiveAppView view, @Nullable String recordingId) { 93 if (view != null && recordingId == null) { 94 throw new IllegalArgumentException( 95 "null recordingId is not allowed only when the view is not null"); 96 } 97 if (view == null && recordingId != null) { 98 throw new IllegalArgumentException( 99 "recordingId should be null when the view is null"); 100 } 101 mTvIAppView = view; 102 mRecordingId = recordingId; 103 } 104 105 /** 106 * Tunes to a given channel for TV program recording. The first tune request will create a new 107 * recording session for the corresponding TV input and establish a connection between the 108 * application and the session. If recording has already started in the current recording 109 * session, this method throws an exception. 110 * 111 * <p>The application may call this method before starting or after stopping recording, but not 112 * during recording. 113 * 114 * <p>The recording session will respond by calling 115 * {@link RecordingCallback#onTuned(Uri)} if the tune request was fulfilled, or 116 * {@link RecordingCallback#onError(int)} otherwise. 117 * 118 * @param inputId The ID of the TV input for the given channel. 119 * @param channelUri The URI of a channel. 120 * @throws IllegalStateException If recording is already started. 121 */ tune(String inputId, Uri channelUri)122 public void tune(String inputId, Uri channelUri) { 123 tune(inputId, channelUri, null); 124 } 125 126 /** 127 * Tunes to a given channel for TV program recording. The first tune request will create a new 128 * recording session for the corresponding TV input and establish a connection between the 129 * application and the session. If recording has already started in the current recording 130 * session, this method throws an exception. This can be used to provide domain-specific 131 * features that are only known between certain client and their TV inputs. 132 * 133 * <p>The application may call this method before starting or after stopping recording, but not 134 * during recording. 135 * 136 * <p>The recording session will respond by calling 137 * {@link RecordingCallback#onTuned(Uri)} if the tune request was fulfilled, or 138 * {@link RecordingCallback#onError(int)} otherwise. 139 * 140 * @param inputId The ID of the TV input for the given channel. 141 * @param channelUri The URI of a channel. 142 * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped 143 * name, i.e. prefixed with a package name you own, so that different developers will 144 * not create conflicting keys. 145 * @throws IllegalStateException If recording is already started. 146 */ tune(String inputId, Uri channelUri, Bundle params)147 public void tune(String inputId, Uri channelUri, Bundle params) { 148 if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); 149 if (TextUtils.isEmpty(inputId)) { 150 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 151 } 152 if (mIsRecordingStarted && !mIsPaused) { 153 throw new IllegalStateException("tune failed - recording already started"); 154 } 155 if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { 156 if (mSession != null) { 157 mSessionCallback.mChannelUri = channelUri; 158 mSession.tune(channelUri, params); 159 } else { 160 mSessionCallback.mChannelUri = channelUri; 161 mSessionCallback.mConnectionParams = params; 162 } 163 mIsTuned = false; 164 } else { 165 if (mIsPaused) { 166 throw new IllegalStateException("tune failed - inputId is changed during pause"); 167 } 168 resetInternal(); 169 mSessionCallback = new MySessionCallback(inputId, channelUri, params); 170 if (mTvInputManager != null) { 171 mTvInputManager.createRecordingSession(inputId, mSessionCallback, mHandler); 172 } 173 } 174 } 175 176 /** 177 * Releases the resources in the current recording session immediately. This may be called at 178 * any time, however if the session is already released, it does nothing. 179 */ release()180 public void release() { 181 if (DEBUG) Log.d(TAG, "release()"); 182 resetInternal(); 183 } 184 resetInternal()185 private void resetInternal() { 186 mSessionCallback = null; 187 mPendingAppPrivateCommands.clear(); 188 if (mSession != null) { 189 mSession.release(); 190 mIsTuned = false; 191 mIsRecordingStarted = false; 192 mIsPaused = false; 193 mIsRecordingStopping = false; 194 mSession = null; 195 } 196 } 197 198 /** 199 * Starts TV program recording in the current recording session. Recording is expected to start 200 * immediately when this method is called. If the current recording session has not yet tuned to 201 * any channel, this method throws an exception. 202 * 203 * <p>The application may supply the URI for a TV program for filling in program specific data 204 * fields in the {@link android.media.tv.TvContract.RecordedPrograms} table. 205 * A non-null {@code programUri} implies the started recording should be of that specific 206 * program, whereas null {@code programUri} does not impose such a requirement and the 207 * recording can span across multiple TV programs. In either case, the application must call 208 * {@link TvRecordingClient#stopRecording()} to stop the recording. 209 * 210 * <p>The recording session will respond by calling {@link RecordingCallback#onError(int)} if 211 * the start request cannot be fulfilled. 212 * 213 * @param programUri The URI for the TV program to record, built by 214 * {@link TvContract#buildProgramUri(long)}. Can be {@code null}. 215 * @throws IllegalStateException If {@link #tune} request hasn't been handled yet or during 216 * pause. 217 */ startRecording(@ullable Uri programUri)218 public void startRecording(@Nullable Uri programUri) { 219 startRecording(programUri, Bundle.EMPTY); 220 } 221 222 /** 223 * Starts TV program recording in the current recording session. Recording is expected to start 224 * immediately when this method is called. If the current recording session has not yet tuned to 225 * any channel, this method throws an exception. 226 * 227 * <p>The application may supply the URI for a TV program for filling in program specific data 228 * fields in the {@link android.media.tv.TvContract.RecordedPrograms} table. 229 * A non-null {@code programUri} implies the started recording should be of that specific 230 * program, whereas null {@code programUri} does not impose such a requirement and the 231 * recording can span across multiple TV programs. In either case, the application must call 232 * {@link TvRecordingClient#stopRecording()} to stop the recording. 233 * 234 * <p>The recording session will respond by calling {@link RecordingCallback#onError(int)} if 235 * the start request cannot be fulfilled. 236 * 237 * @param programUri The URI for the TV program to record, built by 238 * {@link TvContract#buildProgramUri(long)}. Can be {@code null}. 239 * @param params Domain-specific data for this request. Keys <em>must</em> be a scoped 240 * name, i.e. prefixed with a package name you own, so that different developers will 241 * not create conflicting keys. 242 * @throws IllegalStateException If {@link #tune} request hasn't been handled yet or during 243 * pause. 244 */ startRecording(@ullable Uri programUri, @NonNull Bundle params)245 public void startRecording(@Nullable Uri programUri, @NonNull Bundle params) { 246 if (mIsRecordingStopping || !mIsTuned || mIsPaused) { 247 throw new IllegalStateException("startRecording failed -" 248 + "recording not yet stopped or not yet tuned or paused"); 249 } 250 if (mIsRecordingStarted) { 251 Log.w(TAG, "startRecording failed - recording already started"); 252 } 253 if (mSession != null) { 254 mSession.startRecording(programUri, params); 255 mIsRecordingStarted = true; 256 } 257 } 258 259 /** 260 * Stops TV program recording in the current recording session. Recording is expected to stop 261 * immediately when this method is called. If recording has not yet started in the current 262 * recording session, this method does nothing. 263 * 264 * <p>The recording session is expected to create a new data entry in the 265 * {@link android.media.tv.TvContract.RecordedPrograms} table that describes the newly 266 * recorded program and pass the URI to that entry through to 267 * {@link RecordingCallback#onRecordingStopped(Uri)}. 268 * If the stop request cannot be fulfilled, the recording session will respond by calling 269 * {@link RecordingCallback#onError(int)}. 270 */ stopRecording()271 public void stopRecording() { 272 if (!mIsRecordingStarted) { 273 Log.w(TAG, "stopRecording failed - recording not yet started"); 274 } 275 if (mSession != null) { 276 mSession.stopRecording(); 277 if (mIsRecordingStarted) { 278 mIsRecordingStopping = true; 279 } 280 } 281 } 282 283 /** 284 * Pause TV program recording in the current recording session. Recording is expected to pause 285 * immediately when this method is called. If recording has not yet started in the current 286 * recording session, this method does nothing. 287 * 288 * <p>In pause status, the application can tune during recording. To continue recording, 289 * please call {@link TvRecordingClient#resumeRecording()} to resume instead of 290 * {@link TvRecordingClient#startRecording(Uri)}. Application can stop 291 * the recording with {@link TvRecordingClient#stopRecording()} in recording pause status. 292 * 293 * <p>If the pause request cannot be fulfilled, the recording session will respond by calling 294 * {@link RecordingCallback#onError(int)}. 295 */ pauseRecording()296 public void pauseRecording() { 297 pauseRecording(Bundle.EMPTY); 298 } 299 300 /** 301 * Pause TV program recording in the current recording session. Recording is expected to pause 302 * immediately when this method is called. If recording has not yet started in the current 303 * recording session, this method does nothing. 304 * 305 * <p>In pause status, the application can tune during recording. To continue recording, 306 * please call {@link TvRecordingClient#resumeRecording()} to resume instead of 307 * {@link TvRecordingClient#startRecording(Uri)}. Application can stop 308 * the recording with {@link TvRecordingClient#stopRecording()} in recording pause status. 309 * 310 * <p>If the pause request cannot be fulfilled, the recording session will respond by calling 311 * {@link RecordingCallback#onError(int)}. 312 * 313 * @param params Domain-specific data for this request. 314 */ pauseRecording(@onNull Bundle params)315 public void pauseRecording(@NonNull Bundle params) { 316 if (!mIsRecordingStarted || mIsRecordingStopping) { 317 throw new IllegalStateException( 318 "pauseRecording failed - recording not yet started or stopping"); 319 } 320 TvInputInfo info = mTvInputManager.getTvInputInfo(mSessionCallback.mInputId); 321 if (info == null || !info.canPauseRecording()) { 322 throw new UnsupportedOperationException( 323 "pauseRecording failed - operation not supported"); 324 } 325 if (mIsPaused) { 326 Log.w(TAG, "pauseRecording failed - recording already paused"); 327 } 328 if (mSession != null) { 329 mSession.pauseRecording(params); 330 mIsPaused = true; 331 } 332 } 333 334 /** 335 * Resume TV program recording only in recording pause status in the current recording session. 336 * Recording is expected to resume immediately when this method is called. If recording has not 337 * yet paused in the current recording session, this method does nothing. 338 * 339 * <p>When record is resumed, the recording is continue and can not re-tune. Application can 340 * stop the recording with {@link TvRecordingClient#stopRecording()} after record resumed. 341 * 342 * <p>If the pause request cannot be fulfilled, the recording session will respond by calling 343 * {@link RecordingCallback#onError(int)}. 344 */ resumeRecording()345 public void resumeRecording() { 346 resumeRecording(Bundle.EMPTY); 347 } 348 349 /** 350 * Resume TV program recording only in recording pause status in the current recording session. 351 * Recording is expected to resume immediately when this method is called. If recording has not 352 * yet paused in the current recording session, this method does nothing. 353 * 354 * <p>When record is resumed, the recording is continues and can not re-tune. Application can 355 * stop the recording with {@link TvRecordingClient#stopRecording()} after record resumed. 356 * 357 * <p>If the resume request cannot be fulfilled, the recording session will respond by calling 358 * {@link RecordingCallback#onError(int)}. 359 * 360 * @param params Domain-specific data for this request. 361 */ resumeRecording(@onNull Bundle params)362 public void resumeRecording(@NonNull Bundle params) { 363 if (!mIsRecordingStarted || mIsRecordingStopping || !mIsTuned) { 364 throw new IllegalStateException( 365 "resumeRecording failed - recording not yet started or stopping or " 366 + "not yet tuned"); 367 } 368 if (!mIsPaused) { 369 Log.w(TAG, "resumeRecording failed - recording not yet paused"); 370 } 371 if (mSession != null) { 372 mSession.resumeRecording(params); 373 mIsPaused = false; 374 } 375 } 376 377 /** 378 * Sends a private command to the underlying TV input. This can be used to provide 379 * domain-specific features that are only known between certain clients and their TV inputs. 380 * 381 * @param action The name of the private command to send. This <em>must</em> be a scoped name, 382 * i.e. prefixed with a package name you own, so that different developers will not 383 * create conflicting commands. 384 * @param data An optional bundle to send with the command. 385 */ sendAppPrivateCommand(@onNull String action, Bundle data)386 public void sendAppPrivateCommand(@NonNull String action, Bundle data) { 387 if (TextUtils.isEmpty(action)) { 388 throw new IllegalArgumentException("action cannot be null or an empty string"); 389 } 390 if (mSession != null) { 391 mSession.sendAppPrivateCommand(action, data); 392 } else { 393 Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action 394 + "\" pending)"); 395 mPendingAppPrivateCommands.add(Pair.create(action, data)); 396 } 397 } 398 399 // For testing purposes only. 400 /** @hide */ getSessionCallback()401 public TvInputManager.SessionCallback getSessionCallback() { 402 return mSessionCallback; 403 } 404 405 /** 406 * Callback used to receive various status updates on the 407 * {@link android.media.tv.TvInputService.RecordingSession} 408 */ 409 public abstract static class RecordingCallback { 410 /** 411 * This is called when an error occurred while establishing a connection to the recording 412 * session for the corresponding TV input. 413 * 414 * @param inputId The ID of the TV input bound to the current TvRecordingClient. 415 */ onConnectionFailed(String inputId)416 public void onConnectionFailed(String inputId) { 417 } 418 419 /** 420 * This is called when the connection to the current recording session is lost. 421 * 422 * @param inputId The ID of the TV input bound to the current TvRecordingClient. 423 */ onDisconnected(String inputId)424 public void onDisconnected(String inputId) { 425 } 426 427 /** 428 * This is called when the recording session has been tuned to the given channel and is 429 * ready to start recording. 430 * 431 * @param channelUri The URI of a channel. 432 */ onTuned(Uri channelUri)433 public void onTuned(Uri channelUri) { 434 } 435 436 /** 437 * This is called when the current recording session has stopped recording and created a 438 * new data entry in the {@link TvContract.RecordedPrograms} table that describes the newly 439 * recorded program. 440 * 441 * @param recordedProgramUri The URI for the newly recorded program. 442 */ onRecordingStopped(Uri recordedProgramUri)443 public void onRecordingStopped(Uri recordedProgramUri) { 444 } 445 446 /** 447 * This is called when an issue has occurred. It may be called at any time after the current 448 * recording session is created until it is released. 449 * 450 * @param error The error code. Should be one of the followings. 451 * <ul> 452 * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN} 453 * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE} 454 * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY} 455 * </ul> 456 */ onError(@vInputManager.RecordingError int error)457 public void onError(@TvInputManager.RecordingError int error) { 458 } 459 460 /** 461 * This is invoked when a custom event from the bound TV input is sent to this client. 462 * 463 * @param inputId The ID of the TV input bound to this client. 464 * @param eventType The type of the event. 465 * @param eventArgs Optional arguments of the event. 466 * @hide 467 */ 468 @SystemApi onEvent(String inputId, String eventType, Bundle eventArgs)469 public void onEvent(String inputId, String eventType, Bundle eventArgs) { 470 } 471 } 472 473 private class MySessionCallback extends TvInputManager.SessionCallback { 474 final String mInputId; 475 Uri mChannelUri; 476 Bundle mConnectionParams; 477 MySessionCallback(String inputId, Uri channelUri, Bundle connectionParams)478 MySessionCallback(String inputId, Uri channelUri, Bundle connectionParams) { 479 mInputId = inputId; 480 mChannelUri = channelUri; 481 mConnectionParams = connectionParams; 482 } 483 484 @Override onSessionCreated(TvInputManager.Session session)485 public void onSessionCreated(TvInputManager.Session session) { 486 if (DEBUG) { 487 Log.d(TAG, "onSessionCreated()"); 488 } 489 if (this != mSessionCallback) { 490 Log.w(TAG, "onSessionCreated - session already created"); 491 // This callback is obsolete. 492 if (session != null) { 493 session.release(); 494 } 495 return; 496 } 497 mSession = session; 498 if (session != null) { 499 // Sends the pending app private commands. 500 for (Pair<String, Bundle> command : mPendingAppPrivateCommands) { 501 mSession.sendAppPrivateCommand(command.first, command.second); 502 } 503 mPendingAppPrivateCommands.clear(); 504 mSession.tune(mChannelUri, mConnectionParams); 505 } else { 506 mSessionCallback = null; 507 if (mCallback != null) { 508 mCallback.onConnectionFailed(mInputId); 509 } 510 if (mTvIAppView != null) { 511 mTvIAppView.notifyRecordingConnectionFailed(mRecordingId, mInputId); 512 } 513 } 514 } 515 516 @Override onTuned(TvInputManager.Session session, Uri channelUri)517 public void onTuned(TvInputManager.Session session, Uri channelUri) { 518 if (DEBUG) { 519 Log.d(TAG, "onTuned()"); 520 } 521 if (this != mSessionCallback) { 522 Log.w(TAG, "onTuned - session not created"); 523 return; 524 } 525 if (mIsTuned || !Objects.equals(mChannelUri, channelUri)) { 526 Log.w(TAG, "onTuned - already tuned or not yet tuned to last channel"); 527 return; 528 } 529 mIsTuned = true; 530 if (mCallback != null) { 531 mCallback.onTuned(channelUri); 532 } 533 if (mTvIAppView != null) { 534 mTvIAppView.notifyRecordingTuned(mRecordingId, channelUri); 535 } 536 } 537 538 @Override onSessionReleased(TvInputManager.Session session)539 public void onSessionReleased(TvInputManager.Session session) { 540 if (DEBUG) { 541 Log.d(TAG, "onSessionReleased()"); 542 } 543 if (this != mSessionCallback) { 544 Log.w(TAG, "onSessionReleased - session not created"); 545 return; 546 } 547 mIsTuned = false; 548 mIsRecordingStarted = false; 549 mIsPaused = false; 550 mIsRecordingStopping = false; 551 mSessionCallback = null; 552 mSession = null; 553 if (mCallback != null) { 554 mCallback.onDisconnected(mInputId); 555 } 556 if (mTvIAppView != null) { 557 mTvIAppView.notifyRecordingDisconnected(mRecordingId, mInputId); 558 } 559 } 560 561 @Override onRecordingStopped(TvInputManager.Session session, Uri recordedProgramUri)562 public void onRecordingStopped(TvInputManager.Session session, Uri recordedProgramUri) { 563 if (DEBUG) { 564 Log.d(TAG, "onRecordingStopped(recordedProgramUri= " + recordedProgramUri + ")"); 565 } 566 if (this != mSessionCallback) { 567 Log.w(TAG, "onRecordingStopped - session not created"); 568 return; 569 } 570 if (!mIsRecordingStarted) { 571 Log.w(TAG, "onRecordingStopped - recording not yet started"); 572 return; 573 } 574 mIsRecordingStarted = false; 575 mIsPaused = false; 576 mIsRecordingStopping = false; 577 if (mCallback != null) { 578 mCallback.onRecordingStopped(recordedProgramUri); 579 } 580 if (mTvIAppView != null) { 581 mTvIAppView.notifyRecordingStopped(mRecordingId); 582 } 583 } 584 585 @Override onError(TvInputManager.Session session, int error)586 public void onError(TvInputManager.Session session, int error) { 587 if (DEBUG) { 588 Log.d(TAG, "onError(error=" + error + ")"); 589 } 590 if (this != mSessionCallback) { 591 Log.w(TAG, "onError - session not created"); 592 return; 593 } 594 if (mCallback != null) { 595 mCallback.onError(error); 596 } 597 if (mTvIAppView != null) { 598 mTvIAppView.notifyRecordingError(mRecordingId, error); 599 } 600 } 601 602 @Override onSessionEvent(TvInputManager.Session session, String eventType, Bundle eventArgs)603 public void onSessionEvent(TvInputManager.Session session, String eventType, 604 Bundle eventArgs) { 605 if (DEBUG) { 606 Log.d(TAG, "onSessionEvent(eventType=" + eventType + ", eventArgs=" + eventArgs 607 + ")"); 608 } 609 if (this != mSessionCallback) { 610 Log.w(TAG, "onSessionEvent - session not created"); 611 return; 612 } 613 if (mCallback != null) { 614 mCallback.onEvent(mInputId, eventType, eventArgs); 615 } 616 } 617 } 618 } 619