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 17package android.media.tv; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.annotation.SystemApi; 22import android.content.Context; 23import android.media.tv.TvInputManager; 24import android.net.Uri; 25import android.os.Bundle; 26import android.os.Handler; 27import android.os.Looper; 28import android.text.TextUtils; 29import android.util.Log; 30import android.util.Pair; 31 32import java.util.ArrayDeque; 33import java.util.Queue; 34 35/** 36 * The public interface object used to interact with a specific TV input service for TV program 37 * recording. 38 */ 39public class TvRecordingClient { 40 private static final String TAG = "TvRecordingClient"; 41 private static final boolean DEBUG = false; 42 43 private final RecordingCallback mCallback; 44 private final Handler mHandler; 45 46 private final TvInputManager mTvInputManager; 47 private TvInputManager.Session mSession; 48 private MySessionCallback mSessionCallback; 49 50 private boolean mIsRecordingStarted; 51 private boolean mIsTuned; 52 private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>(); 53 54 /** 55 * Creates a new TvRecordingClient object. 56 * 57 * @param context The application context to create a TvRecordingClient with. 58 * @param tag A short name for debugging purposes. 59 * @param callback The callback to receive recording status changes. 60 * @param handler The handler to invoke the callback on. 61 */ 62 public TvRecordingClient(Context context, String tag, @NonNull RecordingCallback callback, 63 Handler handler) { 64 mCallback = callback; 65 mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler; 66 mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); 67 } 68 69 /** 70 * Tunes to a given channel for TV program recording. The first tune request will create a new 71 * recording session for the corresponding TV input and establish a connection between the 72 * application and the session. If recording has already started in the current recording 73 * session, this method throws an exception. 74 * 75 * <p>The application may call this method before starting or after stopping recording, but not 76 * during recording. 77 * 78 * <p>The recording session will respond by calling 79 * {@link RecordingCallback#onTuned(Uri)} if the tune request was fulfilled, or 80 * {@link RecordingCallback#onError(int)} otherwise. 81 * 82 * @param inputId The ID of the TV input for the given channel. 83 * @param channelUri The URI of a channel. 84 * @throws IllegalStateException If recording is already started. 85 */ 86 public void tune(String inputId, Uri channelUri) { 87 tune(inputId, channelUri, null); 88 } 89 90 /** 91 * Tunes to a given channel for TV program recording. The first tune request will create a new 92 * recording session for the corresponding TV input and establish a connection between the 93 * application and the session. If recording has already started in the current recording 94 * session, this method throws an exception. This can be used to provide domain-specific 95 * features that are only known between certain client and their TV inputs. 96 * 97 * <p>The application may call this method before starting or after stopping recording, but not 98 * during recording. 99 * 100 * <p>The recording session will respond by calling 101 * {@link RecordingCallback#onTuned(Uri)} if the tune request was fulfilled, or 102 * {@link RecordingCallback#onError(int)} otherwise. 103 * 104 * @param inputId The ID of the TV input for the given channel. 105 * @param channelUri The URI of a channel. 106 * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped 107 * name, i.e. prefixed with a package name you own, so that different developers will 108 * not create conflicting keys. 109 * @throws IllegalStateException If recording is already started. 110 */ 111 public void tune(String inputId, Uri channelUri, Bundle params) { 112 if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); 113 if (TextUtils.isEmpty(inputId)) { 114 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 115 } 116 if (mIsRecordingStarted) { 117 throw new IllegalStateException("tune failed - recording already started"); 118 } 119 if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { 120 if (mSession != null) { 121 mSession.tune(channelUri, params); 122 } else { 123 mSessionCallback.mChannelUri = channelUri; 124 mSessionCallback.mConnectionParams = params; 125 } 126 } else { 127 resetInternal(); 128 mSessionCallback = new MySessionCallback(inputId, channelUri, params); 129 if (mTvInputManager != null) { 130 mTvInputManager.createRecordingSession(inputId, mSessionCallback, mHandler); 131 } 132 } 133 } 134 135 /** 136 * Releases the resources in the current recording session immediately. This may be called at 137 * any time, however if the session is already released, it does nothing. 138 */ 139 public void release() { 140 if (DEBUG) Log.d(TAG, "release()"); 141 resetInternal(); 142 } 143 144 private void resetInternal() { 145 mSessionCallback = null; 146 mPendingAppPrivateCommands.clear(); 147 if (mSession != null) { 148 mSession.release(); 149 mSession = null; 150 } 151 } 152 153 /** 154 * Starts TV program recording in the current recording session. Recording is expected to start 155 * immediately when this method is called. If the current recording session has not yet tuned to 156 * any channel, this method throws an exception. 157 * 158 * <p>The application may supply the URI for a TV program for filling in program specific data 159 * fields in the {@link android.media.tv.TvContract.RecordedPrograms} table. 160 * A non-null {@code programUri} implies the started recording should be of that specific 161 * program, whereas null {@code programUri} does not impose such a requirement and the 162 * recording can span across multiple TV programs. In either case, the application must call 163 * {@link TvRecordingClient#stopRecording()} to stop the recording. 164 * 165 * <p>The recording session will respond by calling {@link RecordingCallback#onError(int)} if 166 * the start request cannot be fulfilled. 167 * 168 * @param programUri The URI for the TV program to record, built by 169 * {@link TvContract#buildProgramUri(long)}. Can be {@code null}. 170 * @throws IllegalStateException If {@link #tune} request hasn't been handled yet. 171 */ 172 public void startRecording(@Nullable Uri programUri) { 173 if (!mIsTuned) { 174 throw new IllegalStateException("startRecording failed - not yet tuned"); 175 } 176 if (mSession != null) { 177 mSession.startRecording(programUri); 178 mIsRecordingStarted = true; 179 } 180 } 181 182 /** 183 * Stops TV program recording in the current recording session. Recording is expected to stop 184 * immediately when this method is called. If recording has not yet started in the current 185 * recording session, this method does nothing. 186 * 187 * <p>The recording session is expected to create a new data entry in the 188 * {@link android.media.tv.TvContract.RecordedPrograms} table that describes the newly 189 * recorded program and pass the URI to that entry through to 190 * {@link RecordingCallback#onRecordingStopped(Uri)}. 191 * If the stop request cannot be fulfilled, the recording session will respond by calling 192 * {@link RecordingCallback#onError(int)}. 193 */ 194 public void stopRecording() { 195 if (!mIsRecordingStarted) { 196 Log.w(TAG, "stopRecording failed - recording not yet started"); 197 } 198 if (mSession != null) { 199 mSession.stopRecording(); 200 } 201 } 202 203 /** 204 * Sends a private command to the underlying TV input. This can be used to provide 205 * domain-specific features that are only known between certain clients and their TV inputs. 206 * 207 * @param action The name of the private command to send. This <em>must</em> be a scoped name, 208 * i.e. prefixed with a package name you own, so that different developers will not 209 * create conflicting commands. 210 * @param data An optional bundle to send with the command. 211 */ 212 public void sendAppPrivateCommand(@NonNull String action, Bundle data) { 213 if (TextUtils.isEmpty(action)) { 214 throw new IllegalArgumentException("action cannot be null or an empty string"); 215 } 216 if (mSession != null) { 217 mSession.sendAppPrivateCommand(action, data); 218 } else { 219 Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action 220 + "\" pending)"); 221 mPendingAppPrivateCommands.add(Pair.create(action, data)); 222 } 223 } 224 225 /** 226 * Callback used to receive various status updates on the 227 * {@link android.media.tv.TvInputService.RecordingSession} 228 */ 229 public abstract static class RecordingCallback { 230 /** 231 * This is called when an error occurred while establishing a connection to the recording 232 * session for the corresponding TV input. 233 * 234 * @param inputId The ID of the TV input bound to the current TvRecordingClient. 235 */ 236 public void onConnectionFailed(String inputId) { 237 } 238 239 /** 240 * This is called when the connection to the current recording session is lost. 241 * 242 * @param inputId The ID of the TV input bound to the current TvRecordingClient. 243 */ 244 public void onDisconnected(String inputId) { 245 } 246 247 /** 248 * This is called when the recording session has been tuned to the given channel and is 249 * ready to start recording. 250 * 251 * @param channelUri The URI of a channel. 252 */ 253 public void onTuned(Uri channelUri) { 254 } 255 256 /** 257 * This is called when the current recording session has stopped recording and created a 258 * new data entry in the {@link TvContract.RecordedPrograms} table that describes the newly 259 * recorded program. 260 * 261 * @param recordedProgramUri The URI for the newly recorded program. 262 */ 263 public void onRecordingStopped(Uri recordedProgramUri) { 264 } 265 266 /** 267 * This is called when an issue has occurred. It may be called at any time after the current 268 * recording session is created until it is released. 269 * 270 * @param error The error code. Should be one of the followings. 271 * <ul> 272 * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN} 273 * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE} 274 * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY} 275 * </ul> 276 */ 277 public void onError(@TvInputManager.RecordingError int error) { 278 } 279 280 /** 281 * This is invoked when a custom event from the bound TV input is sent to this client. 282 * 283 * @param inputId The ID of the TV input bound to this client. 284 * @param eventType The type of the event. 285 * @param eventArgs Optional arguments of the event. 286 * @hide 287 */ 288 @SystemApi 289 public void onEvent(String inputId, String eventType, Bundle eventArgs) { 290 } 291 } 292 293 private class MySessionCallback extends TvInputManager.SessionCallback { 294 final String mInputId; 295 Uri mChannelUri; 296 Bundle mConnectionParams; 297 298 MySessionCallback(String inputId, Uri channelUri, Bundle connectionParams) { 299 mInputId = inputId; 300 mChannelUri = channelUri; 301 mConnectionParams = connectionParams; 302 } 303 304 @Override 305 public void onSessionCreated(TvInputManager.Session session) { 306 if (DEBUG) { 307 Log.d(TAG, "onSessionCreated()"); 308 } 309 if (this != mSessionCallback) { 310 Log.w(TAG, "onSessionCreated - session already created"); 311 // This callback is obsolete. 312 if (session != null) { 313 session.release(); 314 } 315 return; 316 } 317 mSession = session; 318 if (session != null) { 319 // Sends the pending app private commands. 320 for (Pair<String, Bundle> command : mPendingAppPrivateCommands) { 321 mSession.sendAppPrivateCommand(command.first, command.second); 322 } 323 mPendingAppPrivateCommands.clear(); 324 mSession.tune(mChannelUri, mConnectionParams); 325 } else { 326 mSessionCallback = null; 327 if (mCallback != null) { 328 mCallback.onConnectionFailed(mInputId); 329 } 330 } 331 } 332 333 @Override 334 void onTuned(TvInputManager.Session session, Uri channelUri) { 335 if (DEBUG) { 336 Log.d(TAG, "onTuned()"); 337 } 338 if (this != mSessionCallback) { 339 Log.w(TAG, "onTuned - session not created"); 340 return; 341 } 342 mIsTuned = true; 343 mCallback.onTuned(channelUri); 344 } 345 346 @Override 347 public void onSessionReleased(TvInputManager.Session session) { 348 if (DEBUG) { 349 Log.d(TAG, "onSessionReleased()"); 350 } 351 if (this != mSessionCallback) { 352 Log.w(TAG, "onSessionReleased - session not created"); 353 return; 354 } 355 mIsTuned = false; 356 mIsRecordingStarted = false; 357 mSessionCallback = null; 358 mSession = null; 359 if (mCallback != null) { 360 mCallback.onDisconnected(mInputId); 361 } 362 } 363 364 @Override 365 public void onRecordingStopped(TvInputManager.Session session, Uri recordedProgramUri) { 366 if (DEBUG) { 367 Log.d(TAG, "onRecordingStopped(recordedProgramUri= " + recordedProgramUri + ")"); 368 } 369 if (this != mSessionCallback) { 370 Log.w(TAG, "onRecordingStopped - session not created"); 371 return; 372 } 373 mIsRecordingStarted = false; 374 mCallback.onRecordingStopped(recordedProgramUri); 375 } 376 377 @Override 378 public void onError(TvInputManager.Session session, int error) { 379 if (DEBUG) { 380 Log.d(TAG, "onError(error=" + error + ")"); 381 } 382 if (this != mSessionCallback) { 383 Log.w(TAG, "onError - session not created"); 384 return; 385 } 386 mCallback.onError(error); 387 } 388 389 @Override 390 public void onSessionEvent(TvInputManager.Session session, String eventType, 391 Bundle eventArgs) { 392 if (DEBUG) { 393 Log.d(TAG, "onSessionEvent(eventType=" + eventType + ", eventArgs=" + eventArgs 394 + ")"); 395 } 396 if (this != mSessionCallback) { 397 Log.w(TAG, "onSessionEvent - session not created"); 398 return; 399 } 400 if (mCallback != null) { 401 mCallback.onEvent(mInputId, eventType, eventArgs); 402 } 403 } 404 } 405} 406