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