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