/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.media.tv;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Context;
import android.media.tv.TvInputManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import java.util.ArrayDeque;
import java.util.Queue;

The public interface object used to interact with a specific TV input service for TV program recording.
/** * The public interface object used to interact with a specific TV input service for TV program * recording. */
public class TvRecordingClient { private static final String TAG = "TvRecordingClient"; private static final boolean DEBUG = false; private final RecordingCallback mCallback; private final Handler mHandler; private final TvInputManager mTvInputManager; private TvInputManager.Session mSession; private MySessionCallback mSessionCallback; private boolean mIsRecordingStarted; private boolean mIsTuned; private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>();
Creates a new TvRecordingClient object.
Params:
  • context – The application context to create a TvRecordingClient with.
  • tag – A short name for debugging purposes.
  • callback – The callback to receive recording status changes.
  • handler – The handler to invoke the callback on.
/** * Creates a new TvRecordingClient object. * * @param context The application context to create a TvRecordingClient with. * @param tag A short name for debugging purposes. * @param callback The callback to receive recording status changes. * @param handler The handler to invoke the callback on. */
public TvRecordingClient(Context context, String tag, @NonNull RecordingCallback callback, Handler handler) { mCallback = callback; mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler; mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); }
Tunes to a given channel for TV program recording. The first tune request will create a new recording session for the corresponding TV input and establish a connection between the application and the session. If recording has already started in the current recording session, this method throws an exception.

The application may call this method before starting or after stopping recording, but not during recording.

The recording session will respond by calling RecordingCallback.onTuned(Uri) if the tune request was fulfilled, or RecordingCallback.onError(int) otherwise.

Params:
  • inputId – The ID of the TV input for the given channel.
  • channelUri – The URI of a channel.
Throws:
/** * Tunes to a given channel for TV program recording. The first tune request will create a new * recording session for the corresponding TV input and establish a connection between the * application and the session. If recording has already started in the current recording * session, this method throws an exception. * * <p>The application may call this method before starting or after stopping recording, but not * during recording. * * <p>The recording session will respond by calling * {@link RecordingCallback#onTuned(Uri)} if the tune request was fulfilled, or * {@link RecordingCallback#onError(int)} otherwise. * * @param inputId The ID of the TV input for the given channel. * @param channelUri The URI of a channel. * @throws IllegalStateException If recording is already started. */
public void tune(String inputId, Uri channelUri) { tune(inputId, channelUri, null); }
Tunes to a given channel for TV program recording. The first tune request will create a new recording session for the corresponding TV input and establish a connection between the application and the session. If recording has already started in the current recording session, this method throws an exception. This can be used to provide domain-specific features that are only known between certain client and their TV inputs.

The application may call this method before starting or after stopping recording, but not during recording.

The recording session will respond by calling RecordingCallback.onTuned(Uri) if the tune request was fulfilled, or RecordingCallback.onError(int) otherwise.

Params:
  • inputId – The ID of the TV input for the given channel.
  • channelUri – The URI of a channel.
  • params – Domain-specific data for this tune request. Keys must be a scoped name, i.e. prefixed with a package name you own, so that different developers will not create conflicting keys.
Throws:
/** * Tunes to a given channel for TV program recording. The first tune request will create a new * recording session for the corresponding TV input and establish a connection between the * application and the session. If recording has already started in the current recording * session, this method throws an exception. This can be used to provide domain-specific * features that are only known between certain client and their TV inputs. * * <p>The application may call this method before starting or after stopping recording, but not * during recording. * * <p>The recording session will respond by calling * {@link RecordingCallback#onTuned(Uri)} if the tune request was fulfilled, or * {@link RecordingCallback#onError(int)} otherwise. * * @param inputId The ID of the TV input for the given channel. * @param channelUri The URI of a channel. * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped * name, i.e. prefixed with a package name you own, so that different developers will * not create conflicting keys. * @throws IllegalStateException If recording is already started. */
public void tune(String inputId, Uri channelUri, Bundle params) { if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); if (TextUtils.isEmpty(inputId)) { throw new IllegalArgumentException("inputId cannot be null or an empty string"); } if (mIsRecordingStarted) { throw new IllegalStateException("tune failed - recording already started"); } if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { if (mSession != null) { mSession.tune(channelUri, params); } else { mSessionCallback.mChannelUri = channelUri; mSessionCallback.mConnectionParams = params; } } else { resetInternal(); mSessionCallback = new MySessionCallback(inputId, channelUri, params); if (mTvInputManager != null) { mTvInputManager.createRecordingSession(inputId, mSessionCallback, mHandler); } } }
Releases the resources in the current recording session immediately. This may be called at any time, however if the session is already released, it does nothing.
/** * Releases the resources in the current recording session immediately. This may be called at * any time, however if the session is already released, it does nothing. */
public void release() { if (DEBUG) Log.d(TAG, "release()"); resetInternal(); } private void resetInternal() { mSessionCallback = null; mPendingAppPrivateCommands.clear(); if (mSession != null) { mSession.release(); mSession = null; } }
Starts TV program recording in the current recording session. Recording is expected to start immediately when this method is called. If the current recording session has not yet tuned to any channel, this method throws an exception.

The application may supply the URI for a TV program for filling in program specific data fields in the RecordedPrograms table. A non-null programUri implies the started recording should be of that specific program, whereas null programUri does not impose such a requirement and the recording can span across multiple TV programs. In either case, the application must call stopRecording() to stop the recording.

The recording session will respond by calling RecordingCallback.onError(int) if the start request cannot be fulfilled.

Params:
Throws:
/** * Starts TV program recording in the current recording session. Recording is expected to start * immediately when this method is called. If the current recording session has not yet tuned to * any channel, this method throws an exception. * * <p>The application may supply the URI for a TV program for filling in program specific data * fields in the {@link android.media.tv.TvContract.RecordedPrograms} table. * A non-null {@code programUri} implies the started recording should be of that specific * program, whereas null {@code programUri} does not impose such a requirement and the * recording can span across multiple TV programs. In either case, the application must call * {@link TvRecordingClient#stopRecording()} to stop the recording. * * <p>The recording session will respond by calling {@link RecordingCallback#onError(int)} if * the start request cannot be fulfilled. * * @param programUri The URI for the TV program to record, built by * {@link TvContract#buildProgramUri(long)}. Can be {@code null}. * @throws IllegalStateException If {@link #tune} request hasn't been handled yet. */
public void startRecording(@Nullable Uri programUri) { if (!mIsTuned) { throw new IllegalStateException("startRecording failed - not yet tuned"); } if (mSession != null) { mSession.startRecording(programUri); mIsRecordingStarted = true; } }
Stops TV program recording in the current recording session. Recording is expected to stop immediately when this method is called. If recording has not yet started in the current recording session, this method does nothing.

The recording session is expected to create a new data entry in the RecordedPrograms table that describes the newly recorded program and pass the URI to that entry through to RecordingCallback.onRecordingStopped(Uri). If the stop request cannot be fulfilled, the recording session will respond by calling RecordingCallback.onError(int).

/** * Stops TV program recording in the current recording session. Recording is expected to stop * immediately when this method is called. If recording has not yet started in the current * recording session, this method does nothing. * * <p>The recording session is expected to create a new data entry in the * {@link android.media.tv.TvContract.RecordedPrograms} table that describes the newly * recorded program and pass the URI to that entry through to * {@link RecordingCallback#onRecordingStopped(Uri)}. * If the stop request cannot be fulfilled, the recording session will respond by calling * {@link RecordingCallback#onError(int)}. */
public void stopRecording() { if (!mIsRecordingStarted) { Log.w(TAG, "stopRecording failed - recording not yet started"); } if (mSession != null) { mSession.stopRecording(); } }
Sends a private command to the underlying TV input. This can be used to provide domain-specific features that are only known between certain clients and their TV inputs.
Params:
  • action – The name of the private command to send. This must be a scoped name, i.e. prefixed with a package name you own, so that different developers will not create conflicting commands.
  • data – An optional bundle to send with the command.
/** * Sends a private command to the underlying TV input. This can be used to provide * domain-specific features that are only known between certain clients and their TV inputs. * * @param action The name of the private command to send. This <em>must</em> be a scoped name, * i.e. prefixed with a package name you own, so that different developers will not * create conflicting commands. * @param data An optional bundle to send with the command. */
public void sendAppPrivateCommand(@NonNull String action, Bundle data) { if (TextUtils.isEmpty(action)) { throw new IllegalArgumentException("action cannot be null or an empty string"); } if (mSession != null) { mSession.sendAppPrivateCommand(action, data); } else { Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action + "\" pending)"); mPendingAppPrivateCommands.add(Pair.create(action, data)); } }
Callback used to receive various status updates on the RecordingSession
/** * Callback used to receive various status updates on the * {@link android.media.tv.TvInputService.RecordingSession} */
public abstract static class RecordingCallback {
This is called when an error occurred while establishing a connection to the recording session for the corresponding TV input.
Params:
  • inputId – The ID of the TV input bound to the current TvRecordingClient.
/** * This is called when an error occurred while establishing a connection to the recording * session for the corresponding TV input. * * @param inputId The ID of the TV input bound to the current TvRecordingClient. */
public void onConnectionFailed(String inputId) { }
This is called when the connection to the current recording session is lost.
Params:
  • inputId – The ID of the TV input bound to the current TvRecordingClient.
/** * This is called when the connection to the current recording session is lost. * * @param inputId The ID of the TV input bound to the current TvRecordingClient. */
public void onDisconnected(String inputId) { }
This is called when the recording session has been tuned to the given channel and is ready to start recording.
Params:
  • channelUri – The URI of a channel.
/** * This is called when the recording session has been tuned to the given channel and is * ready to start recording. * * @param channelUri The URI of a channel. */
public void onTuned(Uri channelUri) { }
This is called when the current recording session has stopped recording and created a new data entry in the RecordedPrograms table that describes the newly recorded program.
Params:
  • recordedProgramUri – The URI for the newly recorded program.
/** * This is called when the current recording session has stopped recording and created a * new data entry in the {@link TvContract.RecordedPrograms} table that describes the newly * recorded program. * * @param recordedProgramUri The URI for the newly recorded program. */
public void onRecordingStopped(Uri recordedProgramUri) { }
This is called when an issue has occurred. It may be called at any time after the current recording session is created until it is released.
Params:
/** * This is called when an issue has occurred. It may be called at any time after the current * recording session is created until it is released. * * @param error The error code. Should be one of the followings. * <ul> * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN} * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE} * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY} * </ul> */
public void onError(@TvInputManager.RecordingError int error) { }
This is invoked when a custom event from the bound TV input is sent to this client.
Params:
  • inputId – The ID of the TV input bound to this client.
  • eventType – The type of the event.
  • eventArgs – Optional arguments of the event.
@hide
/** * This is invoked when a custom event from the bound TV input is sent to this client. * * @param inputId The ID of the TV input bound to this client. * @param eventType The type of the event. * @param eventArgs Optional arguments of the event. * @hide */
@SystemApi public void onEvent(String inputId, String eventType, Bundle eventArgs) { } } private class MySessionCallback extends TvInputManager.SessionCallback { final String mInputId; Uri mChannelUri; Bundle mConnectionParams; MySessionCallback(String inputId, Uri channelUri, Bundle connectionParams) { mInputId = inputId; mChannelUri = channelUri; mConnectionParams = connectionParams; } @Override public void onSessionCreated(TvInputManager.Session session) { if (DEBUG) { Log.d(TAG, "onSessionCreated()"); } if (this != mSessionCallback) { Log.w(TAG, "onSessionCreated - session already created"); // This callback is obsolete. if (session != null) { session.release(); } return; } mSession = session; if (session != null) { // Sends the pending app private commands. for (Pair<String, Bundle> command : mPendingAppPrivateCommands) { mSession.sendAppPrivateCommand(command.first, command.second); } mPendingAppPrivateCommands.clear(); mSession.tune(mChannelUri, mConnectionParams); } else { mSessionCallback = null; if (mCallback != null) { mCallback.onConnectionFailed(mInputId); } } } @Override void onTuned(TvInputManager.Session session, Uri channelUri) { if (DEBUG) { Log.d(TAG, "onTuned()"); } if (this != mSessionCallback) { Log.w(TAG, "onTuned - session not created"); return; } mIsTuned = true; mCallback.onTuned(channelUri); } @Override public void onSessionReleased(TvInputManager.Session session) { if (DEBUG) { Log.d(TAG, "onSessionReleased()"); } if (this != mSessionCallback) { Log.w(TAG, "onSessionReleased - session not created"); return; } mIsTuned = false; mIsRecordingStarted = false; mSessionCallback = null; mSession = null; if (mCallback != null) { mCallback.onDisconnected(mInputId); } } @Override public void onRecordingStopped(TvInputManager.Session session, Uri recordedProgramUri) { if (DEBUG) { Log.d(TAG, "onRecordingStopped(recordedProgramUri= " + recordedProgramUri + ")"); } if (this != mSessionCallback) { Log.w(TAG, "onRecordingStopped - session not created"); return; } mIsRecordingStarted = false; mCallback.onRecordingStopped(recordedProgramUri); } @Override public void onError(TvInputManager.Session session, int error) { if (DEBUG) { Log.d(TAG, "onError(error=" + error + ")"); } if (this != mSessionCallback) { Log.w(TAG, "onError - session not created"); return; } mCallback.onError(error); } @Override public void onSessionEvent(TvInputManager.Session session, String eventType, Bundle eventArgs) { if (DEBUG) { Log.d(TAG, "onSessionEvent(eventType=" + eventType + ", eventArgs=" + eventArgs + ")"); } if (this != mSessionCallback) { Log.w(TAG, "onSessionEvent - session not created"); return; } if (mCallback != null) { mCallback.onEvent(mInputId, eventType, eventArgs); } } } }