diff options
Diffstat (limited to 'src/com/android/tv/InputSessionManager.java')
-rw-r--r-- | src/com/android/tv/InputSessionManager.java | 549 |
1 files changed, 549 insertions, 0 deletions
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java new file mode 100644 index 00000000..e4b0f456 --- /dev/null +++ b/src/com/android/tv/InputSessionManager.java @@ -0,0 +1,549 @@ +/* + * Copyright (C) 2015 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 com.android.tv; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputInfo; +import android.media.tv.TvRecordingClient; +import android.media.tv.TvRecordingClient.RecordingCallback; +import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; +import android.media.tv.TvView.TvInputCallback; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; + +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.ui.TunableTvView; +import com.android.tv.ui.TunableTvView.OnTuneListener; +import com.android.tv.util.TvInputManagerHelper; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Manages input sessions. + * Responsible for: + * <ul> + * <li>Manage {@link TvView} sessions and recording sessions</li> + * <li>Manage capabilities (conflict)</li> + * </ul> + * <p> + * As TvView's methods should be called on the main thread and the {@link RecordingSession} should + * look at the state of the {@link TvViewSession} when it calls the framework methods, the framework + * calls in RecordingSession are made on the main thread not to introduce the multi-thread problems. + */ +@TargetApi(Build.VERSION_CODES.N) +public class InputSessionManager { + private static final String TAG = "InputSessionManager"; + private static final boolean DEBUG = false; + + private final Context mContext; + private final TvInputManagerHelper mInputManager; + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private final Set<TvViewSession> mTvViewSessions = new ArraySet<>(); + private final Set<RecordingSession> mRecordingSessions = + Collections.synchronizedSet(new ArraySet<>()); + private final Set<OnTvViewChannelChangeListener> mOnTvViewChannelChangeListeners = + new ArraySet<>(); + + public InputSessionManager(Context context) { + mContext = context.getApplicationContext(); + mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper(); + } + + /** + * Creates the session for {@link TvView}. + * <p> + * Do not call {@link TvView#setCallback} after the session is created. + */ + @MainThread + @NonNull + public TvViewSession createTvViewSession(TvView tvView, TunableTvView tunableTvView, + TvInputCallback callback) { + TvViewSession session = new TvViewSession(tvView, tunableTvView, callback); + mTvViewSessions.add(session); + if (DEBUG) Log.d(TAG, "TvView session created: " + session); + return session; + } + + /** + * Releases the {@link TvView} session. + */ + @MainThread + public void releaseTvViewSession(TvViewSession session) { + mTvViewSessions.remove(session); + session.reset(); + if (DEBUG) Log.d(TAG, "TvView session released: " + session); + } + + /** + * Creates the session for recording. + */ + @NonNull + public RecordingSession createRecordingSession(String inputId, String tag, + RecordingCallback callback, Handler handler, long endTimeMs) { + RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); + mRecordingSessions.add(session); + if (DEBUG) Log.d(TAG, "Recording session created: " + session); + return session; + } + + /** + * Releases the recording session. + */ + public void releaseRecordingSession(RecordingSession session) { + mRecordingSessions.remove(session); + session.release(); + if (DEBUG) Log.d(TAG, "Recording session released: " + session); + } + + /** + * Adds the {@link OnTvViewChannelChangeListener}. + */ + @MainThread + public void addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) { + mOnTvViewChannelChangeListeners.add(listener); + } + + /** + * Removes the {@link OnTvViewChannelChangeListener}. + */ + @MainThread + public void removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) { + mOnTvViewChannelChangeListeners.remove(listener); + } + + @MainThread + void notifyTvViewChannelChange(Uri channelUri) { + for (OnTvViewChannelChangeListener l : mOnTvViewChannelChangeListeners) { + l.onTvViewChannelChange(channelUri); + } + } + + /** + * Returns the current {@link TvView} channel. + */ + @MainThread + public Uri getCurrentTvViewChannelUri() { + for (TvViewSession session : mTvViewSessions) { + if (session.mTuned) { + return session.mChannelUri; + } + } + return null; + } + + /** + * Retruns the earliest end time of recording sessions in progress of the certain TV input. + */ + @MainThread + public Long getEarliestRecordingSessionEndTimeMs(String inputId) { + long timeMs = Long.MAX_VALUE; + synchronized (mRecordingSessions) { + for (RecordingSession session : mRecordingSessions) { + if (session.mTuned && TextUtils.equals(inputId, session.mInputId)) { + if (session.mEndTimeMs < timeMs) { + timeMs = session.mEndTimeMs; + } + } + } + } + return timeMs == Long.MAX_VALUE ? null : timeMs; + } + + @MainThread + int getTunedTvViewSessionCount(String inputId) { + int tunedCount = 0; + for (TvViewSession session : mTvViewSessions) { + if (session.mTuned && Objects.equals(inputId, session.mInputId)) { + ++tunedCount; + } + } + return tunedCount; + } + + @MainThread + boolean isTunedForTvView(Uri channelUri) { + for (TvViewSession session : mTvViewSessions) { + if (session.mTuned && Objects.equals(channelUri, session.mChannelUri)) { + return true; + } + } + return false; + } + + int getTunedRecordingSessionCount(String inputId) { + synchronized (mRecordingSessions) { + int tunedCount = 0; + for (RecordingSession session : mRecordingSessions) { + if (session.mTuned && Objects.equals(inputId, session.mInputId)) { + ++tunedCount; + } + } + return tunedCount; + } + } + + boolean isTunedForRecording(Uri channelUri) { + synchronized (mRecordingSessions) { + for (RecordingSession session : mRecordingSessions) { + if (session.mTuned && Objects.equals(channelUri, session.mChannelUri)) { + return true; + } + } + return false; + } + } + + /** + * The session for {@link TvView}. + * <p> + * The methods which create or release session for the TV input should be called through this + * session. + */ + @MainThread + public class TvViewSession { + private final TvView mTvView; + private final TunableTvView mTunableTvView; + private final TvInputCallback mCallback; + private Channel mChannel; + private String mInputId; + private Uri mChannelUri; + private Bundle mParams; + private OnTuneListener mOnTuneListener; + private boolean mTuned; + private boolean mNeedToBeRetuned; + + TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) { + mTvView = tvView; + mTunableTvView = tunableTvView; + mCallback = callback; + mTvView.setCallback(new DelegateTvInputCallback(mCallback) { + @Override + public void onConnectionFailed(String inputId) { + if (DEBUG) Log.d(TAG, "TvViewSession: commection failed"); + mTuned = false; + mNeedToBeRetuned = false; + super.onConnectionFailed(inputId); + notifyTvViewChannelChange(null); + } + + @Override + public void onDisconnected(String inputId) { + if (DEBUG) Log.d(TAG, "TvViewSession: disconnected"); + mTuned = false; + mNeedToBeRetuned = false; + super.onDisconnected(inputId); + notifyTvViewChannelChange(null); + } + }); + } + + /** + * Tunes to the channel. + * <p> + * As this is called only for the warming up, there's no need to be retuned. + */ + public void tune(String inputId, Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "warm-up tune: {input=" + inputId + ", channelUri=" + channelUri + "}"); + } + mInputId = inputId; + mChannelUri = channelUri; + mTuned = true; + mNeedToBeRetuned = false; + mTvView.tune(inputId, channelUri); + notifyTvViewChannelChange(channelUri); + } + + /** + * Tunes to the channel. + */ + public void tune(Channel channel, Bundle params, OnTuneListener listener) { + if (DEBUG) { + Log.d(TAG, "tune: {session=" + this + ", channel=" + channel + ", params=" + params + + ", listener=" + listener + ", mTuned=" + mTuned + "}"); + } + mChannel = channel; + mInputId = channel.getInputId(); + mChannelUri = channel.getUri(); + mParams = params; + mOnTuneListener = listener; + TvInputInfo input = mInputManager.getTvInputInfo(mInputId); + if (input == null || (input.canRecord() && !isTunedForRecording(mChannelUri) + && getTunedRecordingSessionCount(mInputId) >= input.getTunerCount())) { + if (DEBUG) { + if (input == null) { + Log.d(TAG, "Can't find input for input ID: " + mInputId); + } else { + Log.d(TAG, "No more tuners to tune for input: " + input); + } + } + mCallback.onConnectionFailed(mInputId); + // Release the previous session to not to hold the unnecessary session. + resetByRecording(); + return; + } + mTuned = true; + mNeedToBeRetuned = false; + mTvView.tune(mInputId, mChannelUri, params); + notifyTvViewChannelChange(mChannelUri); + } + + void retune() { + if (DEBUG) Log.d(TAG, "Retune requested."); + if (mNeedToBeRetuned) { + if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}"); + mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener); + mNeedToBeRetuned = false; + } + } + + /** + * Plays a given recorded TV program. + * + * @see TvView#timeShiftPlay + */ + public void timeShiftPlay(String inputId, Uri recordedProgramUri) { + mTuned = false; + mNeedToBeRetuned = false; + mTvView.timeShiftPlay(inputId, recordedProgramUri); + notifyTvViewChannelChange(null); + } + + /** + * Resets this TvView. + */ + public void reset() { + if (DEBUG) Log.d(TAG, "Reset TvView session"); + mTuned = false; + mTvView.reset(); + mNeedToBeRetuned = false; + notifyTvViewChannelChange(null); + } + + void resetByRecording() { + mCallback.onVideoUnavailable(mInputId, + TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE); + if (mTuned) { + if (DEBUG) Log.d(TAG, "Reset TvView session by recording"); + mTunableTvView.resetByRecording(); + reset(); + } + mNeedToBeRetuned = true; + } + } + + /** + * The session for recording. + * <p> + * The caller is responsible for releasing the session when the error occurs. + */ + public class RecordingSession { + private final String mInputId; + private Uri mChannelUri; + private final RecordingCallback mCallback; + private final Handler mHandler; + private volatile long mEndTimeMs; + private TvRecordingClient mClient; + private boolean mTuned; + + RecordingSession(String inputId, String tag, RecordingCallback callback, + Handler handler, long endTimeMs) { + mInputId = inputId; + mCallback = callback; + mHandler = handler; + mClient = new TvRecordingClient(mContext, tag, callback, handler); + mEndTimeMs = endTimeMs; + } + + void release() { + if (DEBUG) Log.d(TAG, "Release of recording session requested."); + runOnHandler(mMainThreadHandler, new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "Releasing of recording session."); + mTuned = false; + mClient.release(); + mClient = null; + for (TvViewSession session : mTvViewSessions) { + if (DEBUG) { + Log.d(TAG, "Finding TvView sessions for retune: {tuned=" + + session.mTuned + ", inputId=" + session.mInputId + + ", session=" + session + "}"); + } + if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) { + session.retune(); + break; + } + } + } + }); + } + + /** + * Tunes to the channel for recording. + */ + public void tune(String inputId, Uri channelUri) { + runOnHandler(mMainThreadHandler, new Runnable() { + @Override + public void run() { + int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId); + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + if (input == null || !input.canRecord() + || input.getTunerCount() <= tunedRecordingSessionCount) { + runOnHandler(mHandler, new Runnable() { + @Override + public void run() { + mCallback.onConnectionFailed(inputId); + } + }); + return; + } + mTuned = true; + int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); + if (!isTunedForTvView(channelUri) && tunedTuneSessionCount > 0 + && tunedRecordingSessionCount + tunedTuneSessionCount + >= input.getTunerCount()) { + for (TvViewSession session : mTvViewSessions) { + if (session.mTuned && Objects.equals(session.mInputId, inputId) + && !isTunedForRecording(session.mChannelUri)) { + session.resetByRecording(); + break; + } + } + } + mChannelUri = channelUri; + mClient.tune(inputId, channelUri); + } + }); + } + + /** + * Starts recording. + */ + public void startRecording(Uri programHintUri) { + mClient.startRecording(programHintUri); + } + + /** + * Stops recording. + */ + public void stopRecording() { + mClient.stopRecording(); + } + + /** + * Sets recording session's ending time. + */ + public void setEndTimeMs(long endTimeMs) { + mEndTimeMs = endTimeMs; + } + + private void runOnHandler(Handler handler, Runnable runnable) { + if (Looper.myLooper() == handler.getLooper()) { + runnable.run(); + } else { + handler.post(runnable); + } + } + } + + private static class DelegateTvInputCallback extends TvInputCallback { + private final TvInputCallback mDelegate; + + DelegateTvInputCallback(TvInputCallback delegate) { + mDelegate = delegate; + } + + @Override + public void onConnectionFailed(String inputId) { + mDelegate.onConnectionFailed(inputId); + } + + @Override + public void onDisconnected(String inputId) { + mDelegate.onDisconnected(inputId); + } + + @Override + public void onChannelRetuned(String inputId, Uri channelUri) { + mDelegate.onChannelRetuned(inputId, channelUri); + } + + @Override + public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { + mDelegate.onTracksChanged(inputId, tracks); + } + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + mDelegate.onTrackSelected(inputId, type, trackId); + } + + @Override + public void onVideoSizeChanged(String inputId, int width, int height) { + mDelegate.onVideoSizeChanged(inputId, width, height); + } + + @Override + public void onVideoAvailable(String inputId) { + mDelegate.onVideoAvailable(inputId); + } + + @Override + public void onVideoUnavailable(String inputId, int reason) { + mDelegate.onVideoUnavailable(inputId, reason); + } + + @Override + public void onContentAllowed(String inputId) { + mDelegate.onContentAllowed(inputId); + } + + @Override + public void onContentBlocked(String inputId, TvContentRating rating) { + mDelegate.onContentBlocked(inputId, rating); + } + + @Override + public void onTimeShiftStatusChanged(String inputId, int status) { + mDelegate.onTimeShiftStatusChanged(inputId, status); + } + } + + /** + * Called when the {@link TvView} channel is changed. + */ + public interface OnTvViewChannelChangeListener { + void onTvViewChannelChange(@Nullable Uri channelUri); + } +} |