| /* |
| * 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. |
| */ |
| 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. |
| * |
| * @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. |
| * |
| * <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. |
| * |
| * <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()} 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 Extra parameters. |
| * @throws IllegalStateException If recording is already started. |
| * @hide |
| */ |
| @SystemApi |
| 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. |
| */ |
| 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. |
| * |
| * <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 programHint} implies the started recording should be of that specific |
| * program, whereas null {@code programHint} 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. |
| * |
| * <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. |
| * |
| * @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 |
| * {@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. |
| * |
| * @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. |
| * |
| * @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. |
| * |
| * @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 {@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. |
| * |
| * @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. |
| * |
| * @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); |
| } |
| } |
| } |
| } |