/* * 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. 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. */ 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 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. * * <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); } } } }