| /* |
| * 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.TvTrackInfo; |
| import android.media.tv.TvView; |
| 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.compat.TvRecordingClientCompat; |
| import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat; |
| import com.android.tv.common.compat.TvViewCompat; |
| import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; |
| import com.android.tv.data.api.Channel; |
| import com.android.tv.dvr.DvrTvView; |
| import com.android.tv.ui.TunableTvView; |
| import com.android.tv.ui.TunableTvView.OnTuneListener; |
| import com.android.tv.ui.api.TunableTvViewPlayingApi; |
| 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>Manage capabilities (conflict) |
| * </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<>(); |
| private final Set<OnRecordingSessionChangeListener> mOnRecordingSessionChangeListeners = |
| new ArraySet<>(); |
| |
| public InputSessionManager(Context context) { |
| mContext = context.getApplicationContext(); |
| mInputManager = TvSingletons.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( |
| TvViewCompat tvView, |
| TunableTvViewPlayingApi tunableTvView, |
| TvInputCallbackCompat 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, |
| RecordingCallbackCompat 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); |
| for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) { |
| listener.onRecordingSessionChange(true, mRecordingSessions.size()); |
| } |
| 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); |
| for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) { |
| listener.onRecordingSessionChange(false, mRecordingSessions.size()); |
| } |
| } |
| |
| /** 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); |
| } |
| } |
| |
| /** Adds the {@link OnRecordingSessionChangeListener}. */ |
| public void addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) { |
| mOnRecordingSessionChangeListeners.add(listener); |
| } |
| |
| /** Removes the {@link OnRecordingSessionChangeListener}. */ |
| public void removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) { |
| mOnRecordingSessionChangeListeners.remove(listener); |
| } |
| |
| /** 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 TvViewCompat mTvView; |
| private final TunableTvViewPlayingApi mTunableTvView; |
| private final TvInputCallbackCompat mCallback; |
| private final boolean mIsDvrSession; |
| private Channel mChannel; |
| private String mInputId; |
| private Uri mChannelUri; |
| private Bundle mParams; |
| private OnTuneListener mOnTuneListener; |
| private boolean mTuned; |
| private boolean mNeedToBeRetuned; |
| |
| TvViewSession( |
| TvViewCompat tvView, |
| TunableTvViewPlayingApi tunableTvView, |
| TvInputCallbackCompat callback) { |
| mTvView = tvView; |
| mTunableTvView = tunableTvView; |
| mCallback = callback; |
| mIsDvrSession = tunableTvView instanceof DvrTvView; |
| mTvView.setCallback( |
| new DelegateTvInputCallback(mCallback) { |
| @Override |
| public void onConnectionFailed(String inputId) { |
| if (DEBUG) Log.d(TAG, "TvViewSession: connection 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 (mIsDvrSession) { |
| Log.w(TAG, "DVR session should not call retune()!"); |
| return; |
| } |
| if (mNeedToBeRetuned) { |
| if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}"); |
| ((TunableTvView) 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 (mIsDvrSession) { |
| Log.w(TAG, "DVR session should not call resetByRecording()!"); |
| return; |
| } |
| if (mTuned) { |
| if (DEBUG) Log.d(TAG, "Reset TvView session by recording"); |
| ((TunableTvView) 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 RecordingCallbackCompat mCallback; |
| private final Handler mHandler; |
| private volatile long mEndTimeMs; |
| private TvRecordingClientCompat mClient; |
| private boolean mTuned; |
| |
| RecordingSession( |
| String inputId, |
| String tag, |
| RecordingCallbackCompat callback, |
| Handler handler, |
| long endTimeMs) { |
| mInputId = inputId; |
| mCallback = callback; |
| mHandler = handler; |
| mClient = new TvRecordingClientCompat(mContext, tag, callback, handler); |
| mEndTimeMs = endTimeMs; |
| } |
| |
| void release() { |
| if (DEBUG) Log.d(TAG, "Release of recording session requested."); |
| runOnHandler( |
| mMainThreadHandler, |
| () -> { |
| 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, |
| () -> { |
| 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 TvInputCallbackCompat { |
| private final TvInputCallbackCompat mDelegate; |
| |
| DelegateTvInputCallback(TvInputCallbackCompat 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); |
| } |
| |
| @Override |
| public void onSignalStrength(String inputId, int value) { |
| mDelegate.onSignalStrength(inputId, value); |
| } |
| } |
| |
| /** Called when the {@link TvView} channel is changed. */ |
| public interface OnTvViewChannelChangeListener { |
| void onTvViewChannelChange(@Nullable Uri channelUri); |
| } |
| |
| /** Called when recording session is created or destroyed. */ |
| public interface OnRecordingSessionChangeListener { |
| void onRecordingSessionChange(boolean create, int count); |
| } |
| } |