android.media.tv.TvInputManager Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* Copyright (C) 2014 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.graphics.Rect;
import android.media.PlaybackParams;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pools.Pool;
import android.util.Pools.SimplePool;
import android.util.SparseArray;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.InputEventSender;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.View;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Central system API to the overall TV input framework (TIF) architecture, which arbitrates
* interaction between applications and the selected TV inputs. You can retrieve an instance of
* this interface with {@link android.content.Context#getSystemService
* Context.getSystemService(Context.TV_INPUT_SERVICE)}.
*
* There are three primary parties involved in the TV input framework (TIF) architecture:
*
*
* - The TV input manager as expressed by this class is the central point of the
* system that manages interaction between all other parts. It is expressed as the client-side API
* here which exists in each application context and communicates with a global system service that
* manages the interaction across all processes.
*
- A TV input implemented by {@link TvInputService} represents an input source
* of TV, which can be a pass-through input such as HDMI, or a tuner input which provides broadcast
* TV programs. The system binds to the TV input per application’s request.
* on implementing TV inputs.
*
- Applications talk to the TV input manager to list TV inputs and check their
* status. Once an application find the input to use, it uses {@link TvView} or
* {@link TvRecordingClient} for further interaction such as watching and recording broadcast TV
* programs.
*
*/
public final class TvInputManager {
private static final String TAG = "TvInputManager";
static final int DVB_DEVICE_START = 0;
static final int DVB_DEVICE_END = 2;
/**
* A demux device of DVB API for controlling the filters of DVB hardware/software.
* @hide
*/
public static final int DVB_DEVICE_DEMUX = DVB_DEVICE_START;
/**
* A DVR device of DVB API for reading transport streams.
* @hide
*/
public static final int DVB_DEVICE_DVR = 1;
/**
* A frontend device of DVB API for controlling the tuner and DVB demodulator hardware.
* @hide
*/
public static final int DVB_DEVICE_FRONTEND = DVB_DEVICE_END;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({VIDEO_UNAVAILABLE_REASON_UNKNOWN, VIDEO_UNAVAILABLE_REASON_TUNING,
VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL, VIDEO_UNAVAILABLE_REASON_BUFFERING,
VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY})
public @interface VideoUnavailableReason {}
static final int VIDEO_UNAVAILABLE_REASON_START = 0;
static final int VIDEO_UNAVAILABLE_REASON_END = 4;
/**
* Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
* {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable due to
* an unspecified error.
*/
public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START;
/**
* Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
* {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
* the corresponding TV input is in the middle of tuning to a new channel.
*/
public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1;
/**
* Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
* {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable due to
* weak TV signal.
*/
public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2;
/**
* Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
* {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
* the corresponding TV input has stopped playback temporarily to buffer more data.
*/
public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = 3;
/**
* Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
* {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
* the current TV program is audio-only.
*/
public static final int VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY = VIDEO_UNAVAILABLE_REASON_END;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({TIME_SHIFT_STATUS_UNKNOWN, TIME_SHIFT_STATUS_UNSUPPORTED,
TIME_SHIFT_STATUS_UNAVAILABLE, TIME_SHIFT_STATUS_AVAILABLE})
public @interface TimeShiftStatus {}
/**
* Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
* {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: Unknown status. Also
* the status prior to calling {@code notifyTimeShiftStatusChanged}.
*/
public static final int TIME_SHIFT_STATUS_UNKNOWN = 0;
/**
* Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
* {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: The current TV input
* does not support time shifting.
*/
public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 1;
/**
* Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
* {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: Time shifting is
* currently unavailable but might work again later.
*/
public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2;
/**
* Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
* {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: Time shifting is
* currently available. In this status, the application assumes it can pause/resume playback,
* seek to a specified time position and set playback rate and audio mode.
*/
public static final int TIME_SHIFT_STATUS_AVAILABLE = 3;
/**
* Value returned by {@link TvInputService.Session#onTimeShiftGetCurrentPosition()} and
* {@link TvInputService.Session#onTimeShiftGetStartPosition()} when time shifting has not
* yet started.
*/
public static final long TIME_SHIFT_INVALID_TIME = Long.MIN_VALUE;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({RECORDING_ERROR_UNKNOWN, RECORDING_ERROR_INSUFFICIENT_SPACE,
RECORDING_ERROR_RESOURCE_BUSY})
public @interface RecordingError {}
static final int RECORDING_ERROR_START = 0;
static final int RECORDING_ERROR_END = 2;
/**
* Error for {@link TvInputService.RecordingSession#notifyError(int)} and
* {@link TvRecordingClient.RecordingCallback#onError(int)}: The requested operation cannot be
* completed due to a problem that does not fit under any other error codes, or the error code
* for the problem is defined on the higher version than application's
* android:targetSdkVersion
.
*/
public static final int RECORDING_ERROR_UNKNOWN = RECORDING_ERROR_START;
/**
* Error for {@link TvInputService.RecordingSession#notifyError(int)} and
* {@link TvRecordingClient.RecordingCallback#onError(int)}: Recording cannot proceed due to
* insufficient storage space.
*/
public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 1;
/**
* Error for {@link TvInputService.RecordingSession#notifyError(int)} and
* {@link TvRecordingClient.RecordingCallback#onError(int)}: Recording cannot proceed because
* a required recording resource was not able to be allocated.
*/
public static final int RECORDING_ERROR_RESOURCE_BUSY = RECORDING_ERROR_END;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({INPUT_STATE_CONNECTED, INPUT_STATE_CONNECTED_STANDBY, INPUT_STATE_DISCONNECTED})
public @interface InputState {}
/**
* State for {@link #getInputState(String)} and
* {@link TvInputCallback#onInputStateChanged(String, int)}: The input source is connected.
*
* This state indicates that a source device is connected to the input port and is in the
* normal operation mode. It is mostly relevant to hardware inputs such as HDMI input. This is
* the default state for any hardware inputs where their states are unknown. Non-hardware inputs
* are considered connected all the time.
*/
public static final int INPUT_STATE_CONNECTED = 0;
/**
* State for {@link #getInputState(String)} and
* {@link TvInputCallback#onInputStateChanged(String, int)}: The input source is connected but
* in standby mode.
*
*
This state indicates that a source device is connected to the input port but is in standby
* mode. It is mostly relevant to hardware inputs such as HDMI input.
*/
public static final int INPUT_STATE_CONNECTED_STANDBY = 1;
/**
* State for {@link #getInputState(String)} and
* {@link TvInputCallback#onInputStateChanged(String, int)}: The input source is disconnected.
*
*
This state indicates that a source device is disconnected from the input port. It is
* mostly relevant to hardware inputs such as HDMI input.
*
*/
public static final int INPUT_STATE_DISCONNECTED = 2;
/**
* Broadcast intent action when the user blocked content ratings change. For use with the
* {@link #isRatingBlocked}.
*/
public static final String ACTION_BLOCKED_RATINGS_CHANGED =
"android.media.tv.action.BLOCKED_RATINGS_CHANGED";
/**
* Broadcast intent action when the parental controls enabled state changes. For use with the
* {@link #isParentalControlsEnabled}.
*/
public static final String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED =
"android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
/**
* Broadcast intent action used to query available content rating systems.
*
*
The TV input manager service locates available content rating systems by querying
* broadcast receivers that are registered for this action. An application can offer additional
* content rating systems to the user by declaring a suitable broadcast receiver in its
* manifest.
*
*
Here is an example broadcast receiver declaration that an application might include in its
* AndroidManifest.xml to advertise custom content rating systems. The meta-data specifies a
* resource that contains a description of each content rating system that is provided by the
* application.
*
*
* {@literal
*
*
*
*
*
* }
*
* In the above example, the @xml/tv_content_rating_systems
resource refers to an
* XML resource whose root element is <rating-system-definitions>
that
* contains zero or more <rating-system-definition>
elements. Each
* <rating-system-definition>
element specifies the ratings, sub-ratings and rating
* orders of a particular content rating system.
*
* @see TvContentRating
*/
public static final String ACTION_QUERY_CONTENT_RATING_SYSTEMS =
"android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
/**
* Content rating systems metadata associated with {@link #ACTION_QUERY_CONTENT_RATING_SYSTEMS}.
*
*
Specifies the resource ID of an XML resource that describes the content rating systems
* that are provided by the application.
*/
public static final String META_DATA_CONTENT_RATING_SYSTEMS =
"android.media.tv.metadata.CONTENT_RATING_SYSTEMS";
/**
* Activity action to set up channel sources i.e. TV inputs of type
* {@link TvInputInfo#TYPE_TUNER}. When invoked, the system will display an appropriate UI for
* the user to initiate the individual setup flow provided by
* {@link android.R.attr#setupActivity} of each TV input service.
*/
public static final String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
private final ITvInputManager mService;
private final Object mLock = new Object();
// @GuardedBy("mLock")
private final List mCallbackRecords = new LinkedList<>();
// A mapping from TV input ID to the state of corresponding input.
// @GuardedBy("mLock")
private final Map mStateMap = new ArrayMap<>();
// A mapping from the sequence number of a session to its SessionCallbackRecord.
private final SparseArray mSessionCallbackRecordMap =
new SparseArray<>();
// A sequence number for the next session to be created. Should be protected by a lock
// {@code mSessionCallbackRecordMap}.
private int mNextSeq;
private final ITvInputClient mClient;
private final int mUserId;
/**
* Interface used to receive the created session.
* @hide
*/
public abstract static class SessionCallback {
/**
* This is called after {@link TvInputManager#createSession} has been processed.
*
* @param session A {@link TvInputManager.Session} instance created. This can be
* {@code null} if the creation request failed.
*/
public void onSessionCreated(@Nullable Session session) {
}
/**
* This is called when {@link TvInputManager.Session} is released.
* This typically happens when the process hosting the session has crashed or been killed.
*
* @param session A {@link TvInputManager.Session} instance released.
*/
public void onSessionReleased(Session session) {
}
/**
* This is called when the channel of this session is changed by the underlying TV input
* without any {@link TvInputManager.Session#tune(Uri)} request.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param channelUri The URI of a channel.
*/
public void onChannelRetuned(Session session, Uri channelUri) {
}
/**
* This is called when the track information of the session has been changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param tracks A list which includes track information.
*/
public void onTracksChanged(Session session, List tracks) {
}
/**
* This is called when a track for a given type is selected.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param type The type of the selected track. The type can be
* {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
* {@link TvTrackInfo#TYPE_SUBTITLE}.
* @param trackId The ID of the selected track. When {@code null} the currently selected
* track for a given type should be unselected.
*/
public void onTrackSelected(Session session, int type, @Nullable String trackId) {
}
/**
* This is invoked when the video size has been changed. It is also called when the first
* time video size information becomes available after the session is tuned to a specific
* channel.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param width The width of the video.
* @param height The height of the video.
*/
public void onVideoSizeChanged(Session session, int width, int height) {
}
/**
* This is called when the video is available, so the TV input starts the playback.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
*/
public void onVideoAvailable(Session session) {
}
/**
* This is called when the video is not available, so the TV input stops the playback.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param reason The reason why the TV input stopped the playback:
*
* - {@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
*
- {@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
*
- {@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
*
- {@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
*
- {@link TvInputManager#VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY}
*
*/
public void onVideoUnavailable(Session session, int reason) {
}
/**
* This is called when the current program content turns out to be allowed to watch since
* its content rating is not blocked by parental controls.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
*/
public void onContentAllowed(Session session) {
}
/**
* This is called when the current program content turns out to be not allowed to watch
* since its content rating is blocked by parental controls.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param rating The content ration of the blocked program.
*/
public void onContentBlocked(Session session, TvContentRating rating) {
}
/**
* This is called when {@link TvInputService.Session#layoutSurface} is called to change the
* layout of surface.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param left Left position.
* @param top Top position.
* @param right Right position.
* @param bottom Bottom position.
*/
public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
}
/**
* This is called when a custom event has been sent from this session.
*
* @param session A {@link TvInputManager.Session} associated with this callback
* @param eventType The type of the event.
* @param eventArgs Optional arguments of the event.
*/
public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
}
/**
* This is called when the time shift status is changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param status The current time shift status. Should be one of the followings.
*
* - {@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
*
- {@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
*
- {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
*
*/
public void onTimeShiftStatusChanged(Session session, int status) {
}
/**
* This is called when the start position for time shifting has changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param timeMs The start position for time shifting, in milliseconds since the epoch.
*/
public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
}
/**
* This is called when the current position for time shifting is changed.
*
* @param session A {@link TvInputManager.Session} associated with this callback.
* @param timeMs The current position for time shifting, in milliseconds since the epoch.
*/
public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
}
// For the recording session only
/**
* 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.
*/
void onTuned(Session session, Uri channelUri) {
}
// For the recording session only
/**
* 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.
**/
void onRecordingStopped(Session session, Uri recordedProgramUri) {
}
// For the recording session only
/**
* 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.
*/
void onError(Session session, @TvInputManager.RecordingError int error) {
}
}
private static final class SessionCallbackRecord {
private final SessionCallback mSessionCallback;
private final Handler mHandler;
private Session mSession;
SessionCallbackRecord(SessionCallback sessionCallback,
Handler handler) {
mSessionCallback = sessionCallback;
mHandler = handler;
}
void postSessionCreated(final Session session) {
mSession = session;
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onSessionCreated(session);
}
});
}
void postSessionReleased() {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onSessionReleased(mSession);
}
});
}
void postChannelRetuned(final Uri channelUri) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onChannelRetuned(mSession, channelUri);
}
});
}
void postTracksChanged(final List tracks) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onTracksChanged(mSession, tracks);
}
});
}
void postTrackSelected(final int type, final String trackId) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onTrackSelected(mSession, type, trackId);
}
});
}
void postVideoSizeChanged(final int width, final int height) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onVideoSizeChanged(mSession, width, height);
}
});
}
void postVideoAvailable() {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onVideoAvailable(mSession);
}
});
}
void postVideoUnavailable(final int reason) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onVideoUnavailable(mSession, reason);
}
});
}
void postContentAllowed() {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onContentAllowed(mSession);
}
});
}
void postContentBlocked(final TvContentRating rating) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onContentBlocked(mSession, rating);
}
});
}
void postLayoutSurface(final int left, final int top, final int right,
final int bottom) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom);
}
});
}
void postSessionEvent(final String eventType, final Bundle eventArgs) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onSessionEvent(mSession, eventType, eventArgs);
}
});
}
void postTimeShiftStatusChanged(final int status) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onTimeShiftStatusChanged(mSession, status);
}
});
}
void postTimeShiftStartPositionChanged(final long timeMs) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onTimeShiftStartPositionChanged(mSession, timeMs);
}
});
}
void postTimeShiftCurrentPositionChanged(final long timeMs) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onTimeShiftCurrentPositionChanged(mSession, timeMs);
}
});
}
// For the recording session only
void postTuned(final Uri channelUri) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onTuned(mSession, channelUri);
}
});
}
// For the recording session only
void postRecordingStopped(final Uri recordedProgramUri) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onRecordingStopped(mSession, recordedProgramUri);
}
});
}
// For the recording session only
void postError(final int error) {
mHandler.post(new Runnable() {
@Override
public void run() {
mSessionCallback.onError(mSession, error);
}
});
}
}
/**
* Callback used to monitor status of the TV inputs.
*/
public abstract static class TvInputCallback {
/**
* This is called when the state of a given TV input is changed.
*
* @param inputId The ID of the TV input.
* @param state State of the TV input. The value is one of the following:
*
* - {@link TvInputManager#INPUT_STATE_CONNECTED}
*
- {@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY}
*
- {@link TvInputManager#INPUT_STATE_DISCONNECTED}
*
*/
public void onInputStateChanged(String inputId, @InputState int state) {
}
/**
* This is called when a TV input is added to the system.
*
* Normally it happens when the user installs a new TV input package that implements
* {@link TvInputService} interface.
*
* @param inputId The ID of the TV input.
*/
public void onInputAdded(String inputId) {
}
/**
* This is called when a TV input is removed from the system.
*
*
Normally it happens when the user uninstalls the previously installed TV input
* package.
*
* @param inputId The ID of the TV input.
*/
public void onInputRemoved(String inputId) {
}
/**
* This is called when a TV input is updated on the system.
*
*
Normally it happens when a previously installed TV input package is re-installed or
* the media on which a newer version of the package exists becomes available/unavailable.
*
* @param inputId The ID of the TV input.
*/
public void onInputUpdated(String inputId) {
}
/**
* This is called when the information about an existing TV input has been updated.
*
*
Because the system automatically creates a TvInputInfo
object for each TV
* input based on the information collected from the AndroidManifest.xml
, this
* method is only called back when such information has changed dynamically.
*
* @param inputInfo The TvInputInfo
object that contains new information.
*/
public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
}
}
private static final class TvInputCallbackRecord {
private final TvInputCallback mCallback;
private final Handler mHandler;
public TvInputCallbackRecord(TvInputCallback callback, Handler handler) {
mCallback = callback;
mHandler = handler;
}
public TvInputCallback getCallback() {
return mCallback;
}
public void postInputAdded(final String inputId) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onInputAdded(inputId);
}
});
}
public void postInputRemoved(final String inputId) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onInputRemoved(inputId);
}
});
}
public void postInputUpdated(final String inputId) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onInputUpdated(inputId);
}
});
}
public void postInputStateChanged(final String inputId, final int state) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onInputStateChanged(inputId, state);
}
});
}
public void postTvInputInfoUpdated(final TvInputInfo inputInfo) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onTvInputInfoUpdated(inputInfo);
}
});
}
}
/**
* Interface used to receive events from Hardware objects.
*
* @hide
*/
@SystemApi
public abstract static class HardwareCallback {
/**
* This is called when {@link Hardware} is no longer available for the client.
*/
public abstract void onReleased();
/**
* This is called when the underlying {@link TvStreamConfig} has been changed.
*
* @param configs The new {@link TvStreamConfig}s.
*/
public abstract void onStreamConfigChanged(TvStreamConfig[] configs);
}
/**
* @hide
*/
public TvInputManager(ITvInputManager service, int userId) {
mService = service;
mUserId = userId;
mClient = new ITvInputClient.Stub() {
@Override
public void onSessionCreated(String inputId, IBinder token, InputChannel channel,
int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for " + token);
return;
}
Session session = null;
if (token != null) {
session = new Session(token, channel, mService, mUserId, seq,
mSessionCallbackRecordMap);
}
record.postSessionCreated(session);
}
}
@Override
public void onSessionReleased(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
mSessionCallbackRecordMap.delete(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq:" + seq);
return;
}
record.mSession.releaseInternal();
record.postSessionReleased();
}
}
@Override
public void onChannelRetuned(Uri channelUri, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postChannelRetuned(channelUri);
}
}
@Override
public void onTracksChanged(List tracks, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
if (record.mSession.updateTracks(tracks)) {
record.postTracksChanged(tracks);
postVideoSizeChangedIfNeededLocked(record);
}
}
}
@Override
public void onTrackSelected(int type, String trackId, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
if (record.mSession.updateTrackSelection(type, trackId)) {
record.postTrackSelected(type, trackId);
postVideoSizeChangedIfNeededLocked(record);
}
}
}
private void postVideoSizeChangedIfNeededLocked(SessionCallbackRecord record) {
TvTrackInfo track = record.mSession.getVideoTrackToNotify();
if (track != null) {
record.postVideoSizeChanged(track.getVideoWidth(), track.getVideoHeight());
}
}
@Override
public void onVideoAvailable(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postVideoAvailable();
}
}
@Override
public void onVideoUnavailable(int reason, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postVideoUnavailable(reason);
}
}
@Override
public void onContentAllowed(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postContentAllowed();
}
}
@Override
public void onContentBlocked(String rating, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postContentBlocked(TvContentRating.unflattenFromString(rating));
}
}
@Override
public void onLayoutSurface(int left, int top, int right, int bottom, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postLayoutSurface(left, top, right, bottom);
}
}
@Override
public void onSessionEvent(String eventType, Bundle eventArgs, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postSessionEvent(eventType, eventArgs);
}
}
@Override
public void onTimeShiftStatusChanged(int status, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postTimeShiftStatusChanged(status);
}
}
@Override
public void onTimeShiftStartPositionChanged(long timeMs, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postTimeShiftStartPositionChanged(timeMs);
}
}
@Override
public void onTimeShiftCurrentPositionChanged(long timeMs, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postTimeShiftCurrentPositionChanged(timeMs);
}
}
@Override
public void onTuned(int seq, Uri channelUri) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postTuned(channelUri);
}
}
@Override
public void onRecordingStopped(Uri recordedProgramUri, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postRecordingStopped(recordedProgramUri);
}
}
@Override
public void onError(int error, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
if (record == null) {
Log.e(TAG, "Callback not found for seq " + seq);
return;
}
record.postError(error);
}
}
};
ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() {
@Override
public void onInputAdded(String inputId) {
synchronized (mLock) {
mStateMap.put(inputId, INPUT_STATE_CONNECTED);
for (TvInputCallbackRecord record : mCallbackRecords) {
record.postInputAdded(inputId);
}
}
}
@Override
public void onInputRemoved(String inputId) {
synchronized (mLock) {
mStateMap.remove(inputId);
for (TvInputCallbackRecord record : mCallbackRecords) {
record.postInputRemoved(inputId);
}
}
}
@Override
public void onInputUpdated(String inputId) {
synchronized (mLock) {
for (TvInputCallbackRecord record : mCallbackRecords) {
record.postInputUpdated(inputId);
}
}
}
@Override
public void onInputStateChanged(String inputId, int state) {
synchronized (mLock) {
mStateMap.put(inputId, state);
for (TvInputCallbackRecord record : mCallbackRecords) {
record.postInputStateChanged(inputId, state);
}
}
}
@Override
public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
synchronized (mLock) {
for (TvInputCallbackRecord record : mCallbackRecords) {
record.postTvInputInfoUpdated(inputInfo);
}
}
}
};
try {
if (mService != null) {
mService.registerCallback(managerCallback, mUserId);
List infos = mService.getTvInputList(mUserId);
synchronized (mLock) {
for (TvInputInfo info : infos) {
String inputId = info.getId();
mStateMap.put(inputId, mService.getTvInputState(inputId, mUserId));
}
}
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the complete list of TV inputs on the system.
*
* @return List of {@link TvInputInfo} for each TV input that describes its meta information.
*/
public List getTvInputList() {
try {
return mService.getTvInputList(mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the {@link TvInputInfo} for a given TV input.
*
* @param inputId The ID of the TV input.
* @return the {@link TvInputInfo} for a given TV input. {@code null} if not found.
*/
@Nullable
public TvInputInfo getTvInputInfo(@NonNull String inputId) {
Preconditions.checkNotNull(inputId);
try {
return mService.getTvInputInfo(inputId, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Updates the TvInputInfo
for an existing TV input. A TV input service
* implementation may call this method to pass the application and system an up-to-date
* TvInputInfo
object that describes itself.
*
* The system automatically creates a TvInputInfo
object for each TV input,
* based on the information collected from the AndroidManifest.xml
, thus it is not
* necessary to call this method unless such information has changed dynamically.
* Use {@link TvInputInfo.Builder} to build a new TvInputInfo
object.
*
*
Attempting to change information about a TV input that the calling package does not own
* does nothing.
*
* @param inputInfo The TvInputInfo
object that contains new information.
* @throws IllegalArgumentException if the argument is {@code null}.
* @see TvInputCallback#onTvInputInfoUpdated(TvInputInfo)
*/
public void updateTvInputInfo(@NonNull TvInputInfo inputInfo) {
Preconditions.checkNotNull(inputInfo);
try {
mService.updateTvInputInfo(inputInfo, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the state of a given TV input.
*
*
The state is one of the following:
*
* - {@link #INPUT_STATE_CONNECTED}
*
- {@link #INPUT_STATE_CONNECTED_STANDBY}
*
- {@link #INPUT_STATE_DISCONNECTED}
*
*
* @param inputId The ID of the TV input.
* @throws IllegalArgumentException if the argument is {@code null}.
*/
@InputState
public int getInputState(@NonNull String inputId) {
Preconditions.checkNotNull(inputId);
synchronized (mLock) {
Integer state = mStateMap.get(inputId);
if (state == null) {
Log.w(TAG, "Unrecognized input ID: " + inputId);
return INPUT_STATE_DISCONNECTED;
}
return state;
}
}
/**
* Registers a {@link TvInputCallback}.
*
* @param callback A callback used to monitor status of the TV inputs.
* @param handler A {@link Handler} that the status change will be delivered to.
*/
public void registerCallback(@NonNull TvInputCallback callback, @NonNull Handler handler) {
Preconditions.checkNotNull(callback);
Preconditions.checkNotNull(handler);
synchronized (mLock) {
mCallbackRecords.add(new TvInputCallbackRecord(callback, handler));
}
}
/**
* Unregisters the existing {@link TvInputCallback}.
*
* @param callback The existing callback to remove.
*/
public void unregisterCallback(@NonNull final TvInputCallback callback) {
Preconditions.checkNotNull(callback);
synchronized (mLock) {
for (Iterator it = mCallbackRecords.iterator();
it.hasNext(); ) {
TvInputCallbackRecord record = it.next();
if (record.getCallback() == callback) {
it.remove();
break;
}
}
}
}
/**
* Returns the user's parental controls enabled state.
*
* @return {@code true} if the user enabled the parental controls, {@code false} otherwise.
*/
public boolean isParentalControlsEnabled() {
try {
return mService.isParentalControlsEnabled(mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Sets the user's parental controls enabled state.
*
* @param enabled The user's parental controls enabled state. {@code true} if the user enabled
* the parental controls, {@code false} otherwise.
* @see #isParentalControlsEnabled
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
public void setParentalControlsEnabled(boolean enabled) {
try {
mService.setParentalControlsEnabled(enabled, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Checks whether a given TV content rating is blocked by the user.
*
* @param rating The TV content rating to check. Can be {@link TvContentRating#UNRATED}.
* @return {@code true} if the given TV content rating is blocked, {@code false} otherwise.
*/
public boolean isRatingBlocked(@NonNull TvContentRating rating) {
Preconditions.checkNotNull(rating);
try {
return mService.isRatingBlocked(rating.flattenToString(), mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the list of blocked content ratings.
*
* @return the list of content ratings blocked by the user.
* @hide
*/
@SystemApi
public List getBlockedRatings() {
try {
List ratings = new ArrayList<>();
for (String rating : mService.getBlockedRatings(mUserId)) {
ratings.add(TvContentRating.unflattenFromString(rating));
}
return ratings;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Adds a user blocked content rating.
*
* @param rating The content rating to block.
* @see #isRatingBlocked
* @see #removeBlockedRating
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
public void addBlockedRating(@NonNull TvContentRating rating) {
Preconditions.checkNotNull(rating);
try {
mService.addBlockedRating(rating.flattenToString(), mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Removes a user blocked content rating.
*
* @param rating The content rating to unblock.
* @see #isRatingBlocked
* @see #addBlockedRating
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
public void removeBlockedRating(@NonNull TvContentRating rating) {
Preconditions.checkNotNull(rating);
try {
mService.removeBlockedRating(rating.flattenToString(), mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the list of all TV content rating systems defined.
* @hide
*/
@SystemApi
public List getTvContentRatingSystemList() {
try {
return mService.getTvContentRatingSystemList(mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Creates a {@link Session} for a given TV input.
*
* The number of sessions that can be created at the same time is limited by the capability
* of the given TV input.
*
* @param inputId The ID of the TV input.
* @param callback A callback used to receive the created session.
* @param handler A {@link Handler} that the session creation will be delivered to.
* @hide
*/
public void createSession(@NonNull String inputId, @NonNull final SessionCallback callback,
@NonNull Handler handler) {
createSessionInternal(inputId, false, callback, handler);
}
/**
* Creates a recording {@link Session} for a given TV input.
*
*
The number of sessions that can be created at the same time is limited by the capability
* of the given TV input.
*
* @param inputId The ID of the TV input.
* @param callback A callback used to receive the created session.
* @param handler A {@link Handler} that the session creation will be delivered to.
* @hide
*/
public void createRecordingSession(@NonNull String inputId,
@NonNull final SessionCallback callback, @NonNull Handler handler) {
createSessionInternal(inputId, true, callback, handler);
}
private void createSessionInternal(String inputId, boolean isRecordingSession,
SessionCallback callback, Handler handler) {
Preconditions.checkNotNull(inputId);
Preconditions.checkNotNull(callback);
Preconditions.checkNotNull(handler);
SessionCallbackRecord record = new SessionCallbackRecord(callback, handler);
synchronized (mSessionCallbackRecordMap) {
int seq = mNextSeq++;
mSessionCallbackRecordMap.put(seq, record);
try {
mService.createSession(mClient, inputId, isRecordingSession, seq, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
/**
* Returns the TvStreamConfig list of the given TV input.
*
* If you are using {@link Hardware} object from {@link
* #acquireTvInputHardware}, you should get the list of available streams
* from {@link HardwareCallback#onStreamConfigChanged} method, not from
* here. This method is designed to be used with {@link #captureFrame} in
* capture scenarios specifically and not suitable for any other use.
*
* @param inputId The ID of the TV input.
* @return List of {@link TvStreamConfig} which is available for capturing
* of the given TV input.
* @hide
*/
@SystemApi
public List getAvailableTvStreamConfigList(String inputId) {
try {
return mService.getAvailableTvStreamConfigList(inputId, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Take a snapshot of the given TV input into the provided Surface.
*
* @param inputId The ID of the TV input.
* @param surface the {@link Surface} to which the snapshot is captured.
* @param config the {@link TvStreamConfig} which is used for capturing.
* @return true when the {@link Surface} is ready to be captured.
* @hide
*/
@SystemApi
public boolean captureFrame(String inputId, Surface surface, TvStreamConfig config) {
try {
return mService.captureFrame(inputId, surface, config, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns true if there is only a single TV input session.
*
* @hide
*/
@SystemApi
public boolean isSingleSessionActive() {
try {
return mService.isSingleSessionActive(mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns a list of TvInputHardwareInfo objects representing available hardware.
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
public List getHardwareList() {
try {
return mService.getHardwareList();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Acquires {@link Hardware} object for the given device ID.
*
* A subsequent call to this method on the same {@code deviceId} will release the currently
* acquired Hardware.
*
* @param deviceId The device ID to acquire Hardware for.
* @param callback A callback to receive updates on Hardware.
* @param info The TV input which will use the acquired Hardware.
* @return Hardware on success, {@code null} otherwise.
*
* @removed
*/
@RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
public Hardware acquireTvInputHardware(int deviceId, final HardwareCallback callback,
TvInputInfo info) {
return acquireTvInputHardware(deviceId, info, callback);
}
/**
* Acquires {@link Hardware} object for the given device ID.
*
*
A subsequent call to this method on the same {@code deviceId} will release the currently
* acquired Hardware.
*
* @param deviceId The device ID to acquire Hardware for.
* @param callback A callback to receive updates on Hardware.
* @param info The TV input which will use the acquired Hardware.
* @return Hardware on success, {@code null} otherwise.
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
public Hardware acquireTvInputHardware(int deviceId, TvInputInfo info,
final HardwareCallback callback) {
try {
return new Hardware(
mService.acquireTvInputHardware(deviceId, new ITvInputHardwareCallback.Stub() {
@Override
public void onReleased() {
callback.onReleased();
}
@Override
public void onStreamConfigChanged(TvStreamConfig[] configs) {
callback.onStreamConfigChanged(configs);
}
}, info, mUserId));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Releases previously acquired hardware object.
*
* @param deviceId The device ID this Hardware was acquired for
* @param hardware Hardware to release.
*
* @hide
*/
@SystemApi
@RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
public void releaseTvInputHardware(int deviceId, Hardware hardware) {
try {
mService.releaseTvInputHardware(deviceId, hardware.getInterface(), mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the list of currently available DVB devices on the system.
*
* @return the list of {@link DvbDeviceInfo} objects representing available DVB devices.
* @hide
*/
public List getDvbDeviceList() {
try {
return mService.getDvbDeviceList();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns a {@link ParcelFileDescriptor} of a specified DVB device for a given
* {@link DvbDeviceInfo}
*
* @param info A {@link DvbDeviceInfo} to open a DVB device.
* @param device A DVB device. The DVB device can be {@link #DVB_DEVICE_DEMUX},
* {@link #DVB_DEVICE_DVR} or {@link #DVB_DEVICE_FRONTEND}.
* @return a {@link ParcelFileDescriptor} of a specified DVB device for a given
* {@link DvbDeviceInfo}, or {@code null} if the given {@link DvbDeviceInfo} was invalid
* or the specified DVB device was busy with a previous request.
* @hide
*/
public ParcelFileDescriptor openDvbDevice(DvbDeviceInfo info, int device) {
try {
if (DVB_DEVICE_START > device || DVB_DEVICE_END < device) {
throw new IllegalArgumentException("Invalid DVB device: " + device);
}
return mService.openDvbDevice(info, device);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* The Session provides the per-session functionality of TV inputs.
* @hide
*/
public static final class Session {
static final int DISPATCH_IN_PROGRESS = -1;
static final int DISPATCH_NOT_HANDLED = 0;
static final int DISPATCH_HANDLED = 1;
private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
private final ITvInputManager mService;
private final int mUserId;
private final int mSeq;
// For scheduling input event handling on the main thread. This also serves as a lock to
// protect pending input events and the input channel.
private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
private final Pool mPendingEventPool = new SimplePool<>(20);
private final SparseArray mPendingEvents = new SparseArray<>(20);
private final SparseArray mSessionCallbackRecordMap;
private IBinder mToken;
private TvInputEventSender mSender;
private InputChannel mChannel;
private final Object mMetadataLock = new Object();
// @GuardedBy("mMetadataLock")
private final List mAudioTracks = new ArrayList<>();
// @GuardedBy("mMetadataLock")
private final List mVideoTracks = new ArrayList<>();
// @GuardedBy("mMetadataLock")
private final List mSubtitleTracks = new ArrayList<>();
// @GuardedBy("mMetadataLock")
private String mSelectedAudioTrackId;
// @GuardedBy("mMetadataLock")
private String mSelectedVideoTrackId;
// @GuardedBy("mMetadataLock")
private String mSelectedSubtitleTrackId;
// @GuardedBy("mMetadataLock")
private int mVideoWidth;
// @GuardedBy("mMetadataLock")
private int mVideoHeight;
private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
int seq, SparseArray sessionCallbackRecordMap) {
mToken = token;
mChannel = channel;
mService = service;
mUserId = userId;
mSeq = seq;
mSessionCallbackRecordMap = sessionCallbackRecordMap;
}
/**
* Releases this session.
*/
public void release() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.releaseSession(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
releaseInternal();
}
/**
* Sets this as the main session. The main session is a session whose corresponding TV
* input determines the HDMI-CEC active source device.
*
* @see TvView#setMain
*/
void setMain() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.setMainSession(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Sets the {@link android.view.Surface} for this session.
*
* @param surface A {@link android.view.Surface} used to render video.
*/
public void setSurface(Surface surface) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
// surface can be null.
try {
mService.setSurface(mToken, surface, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Notifies of any structural changes (format or size) of the surface passed in
* {@link #setSurface}.
*
* @param format The new PixelFormat of the surface.
* @param width The new width of the surface.
* @param height The new height of the surface.
*/
public void dispatchSurfaceChanged(int format, int width, int height) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Sets the relative stream volume of this session to handle a change of audio focus.
*
* @param volume A volume value between 0.0f to 1.0f.
* @throws IllegalArgumentException if the volume value is out of range.
*/
public void setStreamVolume(float volume) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
if (volume < 0.0f || volume > 1.0f) {
throw new IllegalArgumentException("volume should be between 0.0f and 1.0f");
}
mService.setVolume(mToken, volume, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Tunes to a given channel.
*
* @param channelUri The URI of a channel.
*/
public void tune(Uri channelUri) {
tune(channelUri, null);
}
/**
* Tunes to a given channel.
*
* @param channelUri The URI of a channel.
* @param params A set of extra parameters which might be handled with this tune event.
*/
public void tune(@NonNull Uri channelUri, Bundle params) {
Preconditions.checkNotNull(channelUri);
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
synchronized (mMetadataLock) {
mAudioTracks.clear();
mVideoTracks.clear();
mSubtitleTracks.clear();
mSelectedAudioTrackId = null;
mSelectedVideoTrackId = null;
mSelectedSubtitleTrackId = null;
mVideoWidth = 0;
mVideoHeight = 0;
}
try {
mService.tune(mToken, channelUri, params, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Enables or disables the caption for this session.
*
* @param enabled {@code true} to enable, {@code false} to disable.
*/
public void setCaptionEnabled(boolean enabled) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.setCaptionEnabled(mToken, enabled, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Selects a track.
*
* @param type The type of the track to select. The type can be
* {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
* {@link TvTrackInfo#TYPE_SUBTITLE}.
* @param trackId The ID of the track to select. When {@code null}, the currently selected
* track of the given type will be unselected.
* @see #getTracks
*/
public void selectTrack(int type, @Nullable String trackId) {
synchronized (mMetadataLock) {
if (type == TvTrackInfo.TYPE_AUDIO) {
if (trackId != null && !containsTrack(mAudioTracks, trackId)) {
Log.w(TAG, "Invalid audio trackId: " + trackId);
return;
}
} else if (type == TvTrackInfo.TYPE_VIDEO) {
if (trackId != null && !containsTrack(mVideoTracks, trackId)) {
Log.w(TAG, "Invalid video trackId: " + trackId);
return;
}
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
if (trackId != null && !containsTrack(mSubtitleTracks, trackId)) {
Log.w(TAG, "Invalid subtitle trackId: " + trackId);
return;
}
} else {
throw new IllegalArgumentException("invalid type: " + type);
}
}
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.selectTrack(mToken, type, trackId, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private boolean containsTrack(List tracks, String trackId) {
for (TvTrackInfo track : tracks) {
if (track.getId().equals(trackId)) {
return true;
}
}
return false;
}
/**
* Returns the list of tracks for a given type. Returns {@code null} if the information is
* not available.
*
* @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
* {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
* @return the list of tracks for the given type.
*/
@Nullable
public List getTracks(int type) {
synchronized (mMetadataLock) {
if (type == TvTrackInfo.TYPE_AUDIO) {
if (mAudioTracks == null) {
return null;
}
return new ArrayList<>(mAudioTracks);
} else if (type == TvTrackInfo.TYPE_VIDEO) {
if (mVideoTracks == null) {
return null;
}
return new ArrayList<>(mVideoTracks);
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
if (mSubtitleTracks == null) {
return null;
}
return new ArrayList<>(mSubtitleTracks);
}
}
throw new IllegalArgumentException("invalid type: " + type);
}
/**
* Returns the selected track for a given type. Returns {@code null} if the information is
* not available or any of the tracks for the given type is not selected.
*
* @return The ID of the selected track.
* @see #selectTrack
*/
@Nullable
public String getSelectedTrack(int type) {
synchronized (mMetadataLock) {
if (type == TvTrackInfo.TYPE_AUDIO) {
return mSelectedAudioTrackId;
} else if (type == TvTrackInfo.TYPE_VIDEO) {
return mSelectedVideoTrackId;
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
return mSelectedSubtitleTrackId;
}
}
throw new IllegalArgumentException("invalid type: " + type);
}
/**
* Responds to onTracksChanged() and updates the internal track information. Returns true if
* there is an update.
*/
boolean updateTracks(List tracks) {
synchronized (mMetadataLock) {
mAudioTracks.clear();
mVideoTracks.clear();
mSubtitleTracks.clear();
for (TvTrackInfo track : tracks) {
if (track.getType() == TvTrackInfo.TYPE_AUDIO) {
mAudioTracks.add(track);
} else if (track.getType() == TvTrackInfo.TYPE_VIDEO) {
mVideoTracks.add(track);
} else if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
mSubtitleTracks.add(track);
}
}
return !mAudioTracks.isEmpty() || !mVideoTracks.isEmpty()
|| !mSubtitleTracks.isEmpty();
}
}
/**
* Responds to onTrackSelected() and updates the internal track selection information.
* Returns true if there is an update.
*/
boolean updateTrackSelection(int type, String trackId) {
synchronized (mMetadataLock) {
if (type == TvTrackInfo.TYPE_AUDIO
&& !TextUtils.equals(trackId, mSelectedAudioTrackId)) {
mSelectedAudioTrackId = trackId;
return true;
} else if (type == TvTrackInfo.TYPE_VIDEO
&& !TextUtils.equals(trackId, mSelectedVideoTrackId)) {
mSelectedVideoTrackId = trackId;
return true;
} else if (type == TvTrackInfo.TYPE_SUBTITLE
&& !TextUtils.equals(trackId, mSelectedSubtitleTrackId)) {
mSelectedSubtitleTrackId = trackId;
return true;
}
}
return false;
}
/**
* Returns the new/updated video track that contains new video size information. Returns
* null if there is no video track to notify. Subsequent calls of this method results in a
* non-null video track returned only by the first call and null returned by following
* calls. The caller should immediately notify of the video size change upon receiving the
* track.
*/
TvTrackInfo getVideoTrackToNotify() {
synchronized (mMetadataLock) {
if (!mVideoTracks.isEmpty() && mSelectedVideoTrackId != null) {
for (TvTrackInfo track : mVideoTracks) {
if (track.getId().equals(mSelectedVideoTrackId)) {
int videoWidth = track.getVideoWidth();
int videoHeight = track.getVideoHeight();
if (mVideoWidth != videoWidth || mVideoHeight != videoHeight) {
mVideoWidth = videoWidth;
mVideoHeight = videoHeight;
return track;
}
}
}
}
}
return null;
}
/**
* Plays a given recorded TV program.
*/
void timeShiftPlay(Uri recordedProgramUri) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.timeShiftPlay(mToken, recordedProgramUri, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Pauses the playback. Call {@link #timeShiftResume()} to restart the playback.
*/
void timeShiftPause() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.timeShiftPause(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Resumes the playback. No-op if it is already playing the channel.
*/
void timeShiftResume() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.timeShiftResume(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Seeks to a specified time position.
*
* Normally, the position is given within range between the start and the current time,
* inclusively.
*
* @param timeMs The time position to seek to, in milliseconds since the epoch.
* @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged
*/
void timeShiftSeekTo(long timeMs) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.timeShiftSeekTo(mToken, timeMs, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Sets playback rate using {@link android.media.PlaybackParams}.
*
* @param params The playback params.
*/
void timeShiftSetPlaybackParams(PlaybackParams params) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.timeShiftSetPlaybackParams(mToken, params, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Enable/disable position tracking.
*
* @param enable {@code true} to enable tracking, {@code false} otherwise.
*/
void timeShiftEnablePositionTracking(boolean enable) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.timeShiftEnablePositionTracking(mToken, enable, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Starts TV program recording in the current recording session.
*
* @param programUri The URI for the TV program to record as a hint, built by
* {@link TvContract#buildProgramUri(long)}. Can be {@code null}.
*/
void startRecording(@Nullable Uri programUri) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.startRecording(mToken, programUri, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Stops TV program recording in the current recording session.
*/
void stopRecording() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.stopRecording(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
* TvInputService.Session.appPrivateCommand()} on the current TvView.
*
* @param action Name of the command to be performed. This must be a scoped name,
* i.e. prefixed with a package name you own, so that different developers will
* not create conflicting commands.
* @param data Any data to include with the command.
*/
public void sendAppPrivateCommand(String action, Bundle data) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.sendAppPrivateCommand(mToken, action, data, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView}
* should be called whenever the layout of its containing view is changed.
* {@link #removeOverlayView()} should be called to remove the overlay view.
* Since a session can have only one overlay view, this method should be called only once
* or it can be called again after calling {@link #removeOverlayView()}.
*
* @param view A view playing TV.
* @param frame A position of the overlay view.
* @throws IllegalStateException if {@code view} is not attached to a window.
*/
void createOverlayView(@NonNull View view, @NonNull Rect frame) {
Preconditions.checkNotNull(view);
Preconditions.checkNotNull(frame);
if (view.getWindowToken() == null) {
throw new IllegalStateException("view must be attached to a window");
}
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Relayouts the current overlay view.
*
* @param frame A new position of the overlay view.
*/
void relayoutOverlayView(@NonNull Rect frame) {
Preconditions.checkNotNull(frame);
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.relayoutOverlayView(mToken, frame, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Removes the current overlay view.
*/
void removeOverlayView() {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.removeOverlayView(mToken, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Requests to unblock content blocked by parental controls.
*/
void unblockContent(@NonNull TvContentRating unblockedRating) {
Preconditions.checkNotNull(unblockedRating);
if (mToken == null) {
Log.w(TAG, "The session has been already released");
return;
}
try {
mService.unblockContent(mToken, unblockedRating.flattenToString(), mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Dispatches an input event to this session.
*
* @param event An {@link InputEvent} to dispatch. Cannot be {@code null}.
* @param token A token used to identify the input event later in the callback.
* @param callback A callback used to receive the dispatch result. Cannot be {@code null}.
* @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be
* {@code null}.
* @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
* {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
* {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
* be invoked later.
* @hide
*/
public int dispatchInputEvent(@NonNull InputEvent event, Object token,
@NonNull FinishedInputEventCallback callback, @NonNull Handler handler) {
Preconditions.checkNotNull(event);
Preconditions.checkNotNull(callback);
Preconditions.checkNotNull(handler);
synchronized (mHandler) {
if (mChannel == null) {
return DISPATCH_NOT_HANDLED;
}
PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
if (Looper.myLooper() == Looper.getMainLooper()) {
// Already running on the main thread so we can send the event immediately.
return sendInputEventOnMainLooperLocked(p);
}
// Post the event to the main thread.
Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
return DISPATCH_IN_PROGRESS;
}
}
/**
* Callback that is invoked when an input event that was dispatched to this session has been
* finished.
*
* @hide
*/
public interface FinishedInputEventCallback {
/**
* Called when the dispatched input event is finished.
*
* @param token A token passed to {@link #dispatchInputEvent}.
* @param handled {@code true} if the dispatched input event was handled properly.
* {@code false} otherwise.
*/
void onFinishedInputEvent(Object token, boolean handled);
}
// Must be called on the main looper
private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
synchronized (mHandler) {
int result = sendInputEventOnMainLooperLocked(p);
if (result == DISPATCH_IN_PROGRESS) {
return;
}
}
invokeFinishedInputEventCallback(p, false);
}
private int sendInputEventOnMainLooperLocked(PendingEvent p) {
if (mChannel != null) {
if (mSender == null) {
mSender = new TvInputEventSender(mChannel, mHandler.getLooper());
}
final InputEvent event = p.mEvent;
final int seq = event.getSequenceNumber();
if (mSender.sendInputEvent(seq, event)) {
mPendingEvents.put(seq, p);
Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
msg.setAsynchronous(true);
mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
return DISPATCH_IN_PROGRESS;
}
Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
+ event);
}
return DISPATCH_NOT_HANDLED;
}
void finishedInputEvent(int seq, boolean handled, boolean timeout) {
final PendingEvent p;
synchronized (mHandler) {
int index = mPendingEvents.indexOfKey(seq);
if (index < 0) {
return; // spurious, event already finished or timed out
}
p = mPendingEvents.valueAt(index);
mPendingEvents.removeAt(index);
if (timeout) {
Log.w(TAG, "Timeout waiting for session to handle input event after "
+ INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
} else {
mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
}
}
invokeFinishedInputEventCallback(p, handled);
}
// Assumes the event has already been removed from the queue.
void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
p.mHandled = handled;
if (p.mEventHandler.getLooper().isCurrentThread()) {
// Already running on the callback handler thread so we can send the callback
// immediately.
p.run();
} else {
// Post the event to the callback handler thread.
// In this case, the callback will be responsible for recycling the event.
Message msg = Message.obtain(p.mEventHandler, p);
msg.setAsynchronous(true);
msg.sendToTarget();
}
}
private void flushPendingEventsLocked() {
mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
final int count = mPendingEvents.size();
for (int i = 0; i < count; i++) {
int seq = mPendingEvents.keyAt(i);
Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
msg.setAsynchronous(true);
msg.sendToTarget();
}
}
private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
FinishedInputEventCallback callback, Handler handler) {
PendingEvent p = mPendingEventPool.acquire();
if (p == null) {
p = new PendingEvent();
}
p.mEvent = event;
p.mEventToken = token;
p.mCallback = callback;
p.mEventHandler = handler;
return p;
}
private void recyclePendingEventLocked(PendingEvent p) {
p.recycle();
mPendingEventPool.release(p);
}
IBinder getToken() {
return mToken;
}
private void releaseInternal() {
mToken = null;
synchronized (mHandler) {
if (mChannel != null) {
if (mSender != null) {
flushPendingEventsLocked();
mSender.dispose();
mSender = null;
}
mChannel.dispose();
mChannel = null;
}
}
synchronized (mSessionCallbackRecordMap) {
mSessionCallbackRecordMap.remove(mSeq);
}
}
private final class InputEventHandler extends Handler {
public static final int MSG_SEND_INPUT_EVENT = 1;
public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
public static final int MSG_FLUSH_INPUT_EVENT = 3;
InputEventHandler(Looper looper) {
super(looper, null, true);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SEND_INPUT_EVENT: {
sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
return;
}
case MSG_TIMEOUT_INPUT_EVENT: {
finishedInputEvent(msg.arg1, false, true);
return;
}
case MSG_FLUSH_INPUT_EVENT: {
finishedInputEvent(msg.arg1, false, false);
return;
}
}
}
}
private final class TvInputEventSender extends InputEventSender {
public TvInputEventSender(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEventFinished(int seq, boolean handled) {
finishedInputEvent(seq, handled, false);
}
}
private final class PendingEvent implements Runnable {
public InputEvent mEvent;
public Object mEventToken;
public FinishedInputEventCallback mCallback;
public Handler mEventHandler;
public boolean mHandled;
public void recycle() {
mEvent = null;
mEventToken = null;
mCallback = null;
mEventHandler = null;
mHandled = false;
}
@Override
public void run() {
mCallback.onFinishedInputEvent(mEventToken, mHandled);
synchronized (mEventHandler) {
recyclePendingEventLocked(this);
}
}
}
}
/**
* The Hardware provides the per-hardware functionality of TV hardware.
*
*
TV hardware is physical hardware attached to the Android device; for example, HDMI ports,
* Component/Composite ports, etc. Specifically, logical devices such as HDMI CEC logical
* devices don't fall into this category.
*
* @hide
*/
@SystemApi
public final static class Hardware {
private final ITvInputHardware mInterface;
private Hardware(ITvInputHardware hardwareInterface) {
mInterface = hardwareInterface;
}
private ITvInputHardware getInterface() {
return mInterface;
}
public boolean setSurface(Surface surface, TvStreamConfig config) {
try {
return mInterface.setSurface(surface, config);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public void setStreamVolume(float volume) {
try {
mInterface.setStreamVolume(volume);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public boolean dispatchKeyEventToHdmi(KeyEvent event) {
try {
return mInterface.dispatchKeyEventToHdmi(event);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
public void overrideAudioSink(int audioType, String audioAddress, int samplingRate,
int channelMask, int format) {
try {
mInterface.overrideAudioSink(audioType, audioAddress, samplingRate, channelMask,
format);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
}
}