Home | History | Annotate | Download | only in tv
      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.Queue;
     34 
     35 /**
     36  * The public interface object used to interact with a specific TV input service for TV program
     37  * recording.
     38  */
     39 public 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