1442 lines
61 KiB
Java
1442 lines
61 KiB
Java
/*
|
|
* 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.SuppressLint;
|
|
import android.annotation.SystemApi;
|
|
import android.app.Service;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.graphics.PixelFormat;
|
|
import android.graphics.Rect;
|
|
import android.hardware.hdmi.HdmiDeviceInfo;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.Message;
|
|
import android.os.Process;
|
|
import android.os.RemoteCallbackList;
|
|
import android.os.RemoteException;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.Gravity;
|
|
import android.view.InputChannel;
|
|
import android.view.InputDevice;
|
|
import android.view.InputEvent;
|
|
import android.view.InputEventReceiver;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.Surface;
|
|
import android.view.View;
|
|
import android.view.WindowManager;
|
|
import android.view.accessibility.CaptioningManager;
|
|
import android.widget.FrameLayout;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.os.SomeArgs;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* The TvInputService class represents a TV input or source such as HDMI or built-in tuner which
|
|
* provides pass-through video or broadcast TV programs.
|
|
* <p>
|
|
* Applications will not normally use this service themselves, instead relying on the standard
|
|
* interaction provided by {@link TvView}. Those implementing TV input services should normally do
|
|
* so by deriving from this class and providing their own session implementation based on
|
|
* {@link TvInputService.Session}. All TV input services must require that clients hold the
|
|
* {@link android.Manifest.permission#BIND_TV_INPUT} in order to interact with the service; if this
|
|
* permission is not specified in the manifest, the system will refuse to bind to that TV input
|
|
* service.
|
|
* </p>
|
|
*/
|
|
public abstract class TvInputService extends Service {
|
|
private static final boolean DEBUG = false;
|
|
private static final String TAG = "TvInputService";
|
|
|
|
/**
|
|
* This is the interface name that a service implementing a TV input should say that it support
|
|
* -- that is, this is the action it uses for its intent filter. To be supported, the service
|
|
* must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that
|
|
* other applications cannot abuse it.
|
|
*/
|
|
public static final String SERVICE_INTERFACE = "android.media.tv.TvInputService";
|
|
|
|
/**
|
|
* Name under which a TvInputService component publishes information about itself.
|
|
* This meta-data must reference an XML resource containing an
|
|
* <code><{@link android.R.styleable#TvInputService tv-input}></code>
|
|
* tag.
|
|
*/
|
|
public static final String SERVICE_META_DATA = "android.media.tv.input";
|
|
|
|
/**
|
|
* Handler instance to handle request from TV Input Manager Service. Should be run in the main
|
|
* looper to be synchronously run with {@code Session.mHandler}.
|
|
*/
|
|
private final Handler mServiceHandler = new ServiceHandler();
|
|
private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks =
|
|
new RemoteCallbackList<ITvInputServiceCallback>();
|
|
|
|
private TvInputManager mTvInputManager;
|
|
|
|
@Override
|
|
public final IBinder onBind(Intent intent) {
|
|
return new ITvInputService.Stub() {
|
|
@Override
|
|
public void registerCallback(ITvInputServiceCallback cb) {
|
|
if (cb != null) {
|
|
mCallbacks.register(cb);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void unregisterCallback(ITvInputServiceCallback cb) {
|
|
if (cb != null) {
|
|
mCallbacks.unregister(cb);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void createSession(InputChannel channel, ITvInputSessionCallback cb,
|
|
String inputId) {
|
|
if (channel == null) {
|
|
Log.w(TAG, "Creating session without input channel");
|
|
}
|
|
if (cb == null) {
|
|
return;
|
|
}
|
|
SomeArgs args = SomeArgs.obtain();
|
|
args.arg1 = channel;
|
|
args.arg2 = cb;
|
|
args.arg3 = inputId;
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget();
|
|
}
|
|
|
|
@Override
|
|
public void notifyHardwareAdded(TvInputHardwareInfo hardwareInfo) {
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HARDWARE_TV_INPUT,
|
|
hardwareInfo).sendToTarget();
|
|
}
|
|
|
|
@Override
|
|
public void notifyHardwareRemoved(TvInputHardwareInfo hardwareInfo) {
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_REMOVE_HARDWARE_TV_INPUT,
|
|
hardwareInfo).sendToTarget();
|
|
}
|
|
|
|
@Override
|
|
public void notifyHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HDMI_TV_INPUT,
|
|
deviceInfo).sendToTarget();
|
|
}
|
|
|
|
@Override
|
|
public void notifyHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) {
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_REMOVE_HDMI_TV_INPUT,
|
|
deviceInfo).sendToTarget();
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns a concrete implementation of {@link Session}.
|
|
* <p>
|
|
* May return {@code null} if this TV input service fails to create a session for some reason.
|
|
* If TV input represents an external device connected to a hardware TV input,
|
|
* {@link HardwareSession} should be returned.
|
|
* </p>
|
|
* @param inputId The ID of the TV input associated with the session.
|
|
*/
|
|
public abstract Session onCreateSession(String inputId);
|
|
|
|
/**
|
|
* Returns a new {@link TvInputInfo} object if this service is responsible for
|
|
* {@code hardwareInfo}; otherwise, return {@code null}. Override to modify default behavior of
|
|
* ignoring all hardware input.
|
|
*
|
|
* @param hardwareInfo {@link TvInputHardwareInfo} object just added.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public TvInputInfo onHardwareAdded(TvInputHardwareInfo hardwareInfo) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the input ID for {@code deviceId} if it is handled by this service;
|
|
* otherwise, return {@code null}. Override to modify default behavior of ignoring all hardware
|
|
* input.
|
|
*
|
|
* @param hardwareInfo {@link TvInputHardwareInfo} object just removed.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public String onHardwareRemoved(TvInputHardwareInfo hardwareInfo) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns a new {@link TvInputInfo} object if this service is responsible for
|
|
* {@code deviceInfo}; otherwise, return {@code null}. Override to modify default behavior of
|
|
* ignoring all HDMI logical input device.
|
|
*
|
|
* @param deviceInfo {@link HdmiDeviceInfo} object just added.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public TvInputInfo onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the input ID for {@code deviceInfo} if it is handled by this service; otherwise,
|
|
* return {@code null}. Override to modify default behavior of ignoring all HDMI logical input
|
|
* device.
|
|
*
|
|
* @param deviceInfo {@link HdmiDeviceInfo} object just removed.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public String onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) {
|
|
return null;
|
|
}
|
|
|
|
private boolean isPassthroughInput(String inputId) {
|
|
if (mTvInputManager == null) {
|
|
mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE);
|
|
}
|
|
TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
|
|
if (info != null && info.isPassthroughInput()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Base class for derived classes to implement to provide a TV input session.
|
|
*/
|
|
public abstract static class Session implements KeyEvent.Callback {
|
|
private static final int DETACH_OVERLAY_VIEW_TIMEOUT = 5000;
|
|
private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState();
|
|
private final WindowManager mWindowManager;
|
|
final Handler mHandler;
|
|
private WindowManager.LayoutParams mWindowParams;
|
|
private Surface mSurface;
|
|
private Context mContext;
|
|
private FrameLayout mOverlayViewContainer;
|
|
private View mOverlayView;
|
|
private OverlayViewCleanUpTask mOverlayViewCleanUpTask;
|
|
private boolean mOverlayViewEnabled;
|
|
private IBinder mWindowToken;
|
|
private Rect mOverlayFrame;
|
|
|
|
private Object mLock = new Object();
|
|
// @GuardedBy("mLock")
|
|
private ITvInputSessionCallback mSessionCallback;
|
|
// @GuardedBy("mLock")
|
|
private List<Runnable> mPendingActions = new ArrayList<>();
|
|
|
|
/**
|
|
* Creates a new Session.
|
|
*
|
|
* @param context The context of the application
|
|
*/
|
|
public Session(Context context) {
|
|
mContext = context;
|
|
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
|
mHandler = new Handler(context.getMainLooper());
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the overlay view. By default, the overlay view is disabled. Must be
|
|
* called explicitly after the session is created to enable the overlay view.
|
|
*
|
|
* @param enable {@code true} if you want to enable the overlay view. {@code false}
|
|
* otherwise.
|
|
*/
|
|
public void setOverlayViewEnabled(final boolean enable) {
|
|
mHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (enable == mOverlayViewEnabled) {
|
|
return;
|
|
}
|
|
mOverlayViewEnabled = enable;
|
|
if (enable) {
|
|
if (mWindowToken != null) {
|
|
createOverlayView(mWindowToken, mOverlayFrame);
|
|
}
|
|
} else {
|
|
removeOverlayView(false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dispatches an event to the application using this session.
|
|
*
|
|
* @param eventType The type of the event.
|
|
* @param eventArgs Optional arguments of the event.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public void notifySessionEvent(final String eventType, final Bundle eventArgs) {
|
|
if (eventType == null) {
|
|
throw new IllegalArgumentException("eventType should not be null.");
|
|
}
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifySessionEvent(" + eventType + ")");
|
|
mSessionCallback.onSessionEvent(eventType, eventArgs);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in sending event (event=" + eventType + ")");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notifies the channel of the session is retuned by TV input.
|
|
*
|
|
* @param channelUri The URI of a channel.
|
|
*/
|
|
public void notifyChannelRetuned(final Uri channelUri) {
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyChannelRetuned");
|
|
mSessionCallback.onChannelRetuned(channelUri);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyChannelRetuned");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends the list of all audio/video/subtitle tracks. The is used by the framework to
|
|
* maintain the track information for a given session, which in turn is used by
|
|
* {@link TvView#getTracks} for the application to retrieve metadata for a given track type.
|
|
* The TV input service must call this method as soon as the track information becomes
|
|
* available or is updated. Note that in a case where a part of the information for a
|
|
* certain track is updated, it is not necessary to create a new {@link TvTrackInfo} object
|
|
* with a different track ID.
|
|
*
|
|
* @param tracks A list which includes track information.
|
|
* @throws IllegalArgumentException if {@code tracks} contains redundant tracks.
|
|
*/
|
|
public void notifyTracksChanged(final List<TvTrackInfo> tracks) {
|
|
Set<String> trackIdSet = new HashSet<String>();
|
|
for (TvTrackInfo track : tracks) {
|
|
String trackId = track.getId();
|
|
if (trackIdSet.contains(trackId)) {
|
|
throw new IllegalArgumentException("redundant track ID: " + trackId);
|
|
}
|
|
trackIdSet.add(trackId);
|
|
}
|
|
trackIdSet.clear();
|
|
|
|
// TODO: Validate the track list.
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyTracksChanged");
|
|
mSessionCallback.onTracksChanged(tracks);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyTracksChanged");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends the type and ID of a selected track. This is used to inform the application that a
|
|
* specific track is selected. The TV input service must call this method as soon as a track
|
|
* is selected either by default or in response to a call to {@link #onSelectTrack}. The
|
|
* selected track ID for a given type is maintained in the framework until the next call to
|
|
* this method even after the entire track list is updated (but is reset when the session is
|
|
* tuned to a new channel), so care must be taken not to result in an obsolete track ID.
|
|
*
|
|
* @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.
|
|
* @see #onSelectTrack
|
|
*/
|
|
public void notifyTrackSelected(final int type, final String trackId) {
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyTrackSelected");
|
|
mSessionCallback.onTrackSelected(type, trackId);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyTrackSelected");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Informs the application that the video is now available for watching. This is primarily
|
|
* used to signal the application to unblock the screen. The TV input service must call this
|
|
* method as soon as the content rendered onto its surface gets ready for viewing.
|
|
*
|
|
* @see #notifyVideoUnavailable
|
|
*/
|
|
public void notifyVideoAvailable() {
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyVideoAvailable");
|
|
mSessionCallback.onVideoAvailable();
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyVideoAvailable");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Informs the application that the video became unavailable for some reason. This is
|
|
* primarily used to signal the application to block the screen not to show any intermittent
|
|
* video artifacts.
|
|
*
|
|
* @param reason The reason why the video became unavailable:
|
|
* <ul>
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
|
|
* </ul>
|
|
* @see #notifyVideoAvailable
|
|
*/
|
|
public void notifyVideoUnavailable(final int reason) {
|
|
if (reason < TvInputManager.VIDEO_UNAVAILABLE_REASON_START
|
|
|| reason > TvInputManager.VIDEO_UNAVAILABLE_REASON_END) {
|
|
throw new IllegalArgumentException("Unknown reason: " + reason);
|
|
}
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyVideoUnavailable");
|
|
mSessionCallback.onVideoUnavailable(reason);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyVideoUnavailable");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Informs the application that the user is allowed to watch the current program content.
|
|
* <p>
|
|
* Each TV input service is required to query the system whether the user is allowed to
|
|
* watch the current program before showing it to the user if the parental controls is
|
|
* enabled (i.e. {@link TvInputManager#isParentalControlsEnabled
|
|
* TvInputManager.isParentalControlsEnabled()} returns {@code true}). Whether the TV input
|
|
* service should block the content or not is determined by invoking
|
|
* {@link TvInputManager#isRatingBlocked TvInputManager.isRatingBlocked(TvContentRating)}
|
|
* with the content rating for the current program. Then the {@link TvInputManager} makes a
|
|
* judgment based on the user blocked ratings stored in the secure settings and returns the
|
|
* result. If the rating in question turns out to be allowed by the user, the TV input
|
|
* service must call this method to notify the application that is permitted to show the
|
|
* content.
|
|
* </p><p>
|
|
* Each TV input service also needs to continuously listen to any changes made to the
|
|
* parental controls settings by registering a broadcast receiver to receive
|
|
* {@link TvInputManager#ACTION_BLOCKED_RATINGS_CHANGED} and
|
|
* {@link TvInputManager#ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED} and immediately
|
|
* reevaluate the current program with the new parental controls settings.
|
|
* </p>
|
|
*
|
|
* @see #notifyContentBlocked
|
|
* @see TvInputManager
|
|
*/
|
|
public void notifyContentAllowed() {
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyContentAllowed");
|
|
mSessionCallback.onContentAllowed();
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyContentAllowed");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Informs the application that the current program content is blocked by parent controls.
|
|
* <p>
|
|
* Each TV input service is required to query the system whether the user is allowed to
|
|
* watch the current program before showing it to the user if the parental controls is
|
|
* enabled (i.e. {@link TvInputManager#isParentalControlsEnabled
|
|
* TvInputManager.isParentalControlsEnabled()} returns {@code true}). Whether the TV input
|
|
* service should block the content or not is determined by invoking
|
|
* {@link TvInputManager#isRatingBlocked TvInputManager.isRatingBlocked(TvContentRating)}
|
|
* with the content rating for the current program. Then the {@link TvInputManager} makes a
|
|
* judgment based on the user blocked ratings stored in the secure settings and returns the
|
|
* result. If the rating in question turns out to be blocked, the TV input service must
|
|
* immediately block the content and call this method with the content rating of the current
|
|
* program to prompt the PIN verification screen.
|
|
* </p><p>
|
|
* Each TV input service also needs to continuously listen to any changes made to the
|
|
* parental controls settings by registering a broadcast receiver to receive
|
|
* {@link TvInputManager#ACTION_BLOCKED_RATINGS_CHANGED} and
|
|
* {@link TvInputManager#ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED} and immediately
|
|
* reevaluate the current program with the new parental controls settings.
|
|
* </p>
|
|
*
|
|
* @param rating The content rating for the current TV program.
|
|
* @see #notifyContentAllowed
|
|
* @see TvInputManager
|
|
*/
|
|
public void notifyContentBlocked(final TvContentRating rating) {
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "notifyContentBlocked");
|
|
mSessionCallback.onContentBlocked(rating.flattenToString());
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in notifyContentBlocked");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assigns a position of the {@link Surface} passed by {@link #onSetSurface}. The position
|
|
* is relative to an overlay view.
|
|
*
|
|
* @param left Left position in pixels, relative to the overlay view.
|
|
* @param top Top position in pixels, relative to the overlay view.
|
|
* @param right Right position in pixels, relative to the overlay view.
|
|
* @param bottom Bottom position in pixels, relative to the overlay view.
|
|
* @see #onOverlayViewSizeChanged
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public void layoutSurface(final int left, final int top, final int right,
|
|
final int bottom) {
|
|
if (left > right || top > bottom) {
|
|
throw new IllegalArgumentException("Invalid parameter");
|
|
}
|
|
executeOrPostRunnable(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
if (DEBUG) Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + ", r="
|
|
+ right + ", b=" + bottom + ",)");
|
|
mSessionCallback.onLayoutSurface(left, top, right, bottom);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "error in layoutSurface");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called when the session is released.
|
|
*/
|
|
public abstract void onRelease();
|
|
|
|
/**
|
|
* Sets the current session as the main session. The main session is a session whose
|
|
* corresponding TV input determines the HDMI-CEC active source device.
|
|
* <p>
|
|
* TV input service that manages HDMI-CEC logical device should implement {@link
|
|
* #onSetMain} to (1) select the corresponding HDMI logical device as the source device
|
|
* when {@code isMain} is {@code true}, and to (2) select the internal device (= TV itself)
|
|
* as the source device when {@code isMain} is {@code false} and the session is still main.
|
|
* Also, if a surface is passed to a non-main session and active source is changed to
|
|
* initiate the surface, the active source should be returned to the main session.
|
|
* </p><p>
|
|
* {@link TvView} guarantees that, when tuning involves a session transition, {@code
|
|
* onSetMain(true)} for new session is called first, {@code onSetMain(false)} for old
|
|
* session is called afterwards. This allows {@code onSetMain(false)} to be no-op when TV
|
|
* input service knows that the next main session corresponds to another HDMI logical
|
|
* device. Practically, this implies that one TV input service should handle all HDMI port
|
|
* and HDMI-CEC logical devices for smooth active source transition.
|
|
* </p>
|
|
*
|
|
* @param isMain If true, session should become main.
|
|
* @see TvView#setMain
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public void onSetMain(boolean isMain) {
|
|
}
|
|
|
|
/**
|
|
* Sets the {@link Surface} for the current input session on which the TV input renders
|
|
* video.
|
|
*
|
|
* @param surface {@link Surface} an application passes to this TV input session.
|
|
* @return {@code true} if the surface was set, {@code false} otherwise.
|
|
*/
|
|
public abstract boolean onSetSurface(Surface surface);
|
|
|
|
/**
|
|
* Called after any structural changes (format or size) have been made to the
|
|
* {@link Surface} passed by {@link #onSetSurface}. This method is always called
|
|
* at least once, after {@link #onSetSurface} with non-null {@link Surface} is called.
|
|
*
|
|
* @param format The new PixelFormat of the {@link Surface}.
|
|
* @param width The new width of the {@link Surface}.
|
|
* @param height The new height of the {@link Surface}.
|
|
*/
|
|
public void onSurfaceChanged(int format, int width, int height) {
|
|
}
|
|
|
|
/**
|
|
* Called when a size of an overlay view is changed by an application. Even when the overlay
|
|
* view is disabled by {@link #setOverlayViewEnabled}, this is called. The size is same as
|
|
* the size of {@link Surface} in general. Once {@link #layoutSurface} is called, the sizes
|
|
* of {@link Surface} and the overlay view can be different.
|
|
*
|
|
* @param width The width of the overlay view.
|
|
* @param height The height of the overlay view.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public void onOverlayViewSizeChanged(int width, int height) {
|
|
}
|
|
|
|
/**
|
|
* Sets the relative stream volume of the current TV input session to handle the change of
|
|
* audio focus by setting.
|
|
*
|
|
* @param volume Volume scale from 0.0 to 1.0.
|
|
*/
|
|
public abstract void onSetStreamVolume(float volume);
|
|
|
|
/**
|
|
* Tunes to a given channel. When the video is available, {@link #notifyVideoAvailable()}
|
|
* should be called. Also, {@link #notifyVideoUnavailable(int)} should be called when the
|
|
* TV input cannot continue playing the given channel.
|
|
*
|
|
* @param channelUri The URI of the channel.
|
|
* @return {@code true} the tuning was successful, {@code false} otherwise.
|
|
*/
|
|
public abstract boolean onTune(Uri channelUri);
|
|
|
|
/**
|
|
* Calls {@link #onTune(Uri)}. Override this method in order to handle {@code params}.
|
|
*
|
|
* @param channelUri The URI of the channel.
|
|
* @param params The extra parameters from other applications.
|
|
* @return {@code true} the tuning was successful, {@code false} otherwise.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public boolean onTune(Uri channelUri, Bundle params) {
|
|
return onTune(channelUri);
|
|
}
|
|
|
|
/**
|
|
* Enables or disables the caption.
|
|
* <p>
|
|
* The locale for the user's preferred captioning language can be obtained by calling
|
|
* {@link CaptioningManager#getLocale CaptioningManager.getLocale()}.
|
|
*
|
|
* @param enabled {@code true} to enable, {@code false} to disable.
|
|
* @see CaptioningManager
|
|
*/
|
|
public abstract void onSetCaptionEnabled(boolean enabled);
|
|
|
|
/**
|
|
* Requests to unblock the content according to the given rating.
|
|
* <p>
|
|
* The implementation should unblock the content.
|
|
* TV input service has responsibility to decide when/how the unblock expires
|
|
* while it can keep previously unblocked ratings in order not to ask a user
|
|
* to unblock whenever a content rating is changed.
|
|
* Therefore an unblocked rating can be valid for a channel, a program,
|
|
* or certain amount of time depending on the implementation.
|
|
* </p>
|
|
*
|
|
* @param unblockedRating An unblocked content rating
|
|
*/
|
|
public void onUnblockContent(TvContentRating unblockedRating) {
|
|
}
|
|
|
|
/**
|
|
* Select a given track.
|
|
* <p>
|
|
* If this is done successfully, the implementation should call {@link #notifyTrackSelected}
|
|
* to help applications maintain the selcted track lists.
|
|
* </p>
|
|
*
|
|
* @param trackId The ID of the track to select. {@code null} means to unselect the current
|
|
* track for a given type.
|
|
* @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}.
|
|
* @see #notifyTrackSelected
|
|
*/
|
|
public boolean onSelectTrack(int type, String trackId) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Processes a private command sent from the application to the TV input. This can be used
|
|
* to provide domain-specific features that are only known between certain TV inputs and
|
|
* their clients.
|
|
*
|
|
* @param action Name of the command to be performed. This <em>must</em> 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.
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public void onAppPrivateCommand(String action, Bundle data) {
|
|
}
|
|
|
|
/**
|
|
* Called when an application requests to create an overlay view. Each session
|
|
* implementation can override this method and return its own view.
|
|
*
|
|
* @return a view attached to the overlay window
|
|
*/
|
|
public View onCreateOverlayView() {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent)
|
|
* KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event).
|
|
* <p>
|
|
* Override this to intercept key down events before they are processed by the application.
|
|
* If you return true, the application will not process the event itself. If you return
|
|
* false, the normal application processing will occur as if the TV input had not seen the
|
|
* event at all.
|
|
*
|
|
* @param keyCode The value in event.getKeyCode().
|
|
* @param event Description of the key event.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
*/
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Default implementation of
|
|
* {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
|
|
* KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event).
|
|
* <p>
|
|
* Override this to intercept key long press events before they are processed by the
|
|
* application. If you return true, the application will not process the event itself. If
|
|
* you return false, the normal application processing will occur as if the TV input had not
|
|
* seen the event at all.
|
|
*
|
|
* @param keyCode The value in event.getKeyCode().
|
|
* @param event Description of the key event.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
*/
|
|
@Override
|
|
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Default implementation of
|
|
* {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent)
|
|
* KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event).
|
|
* <p>
|
|
* Override this to intercept special key multiple events before they are processed by the
|
|
* application. If you return true, the application will not itself process the event. If
|
|
* you return false, the normal application processing will occur as if the TV input had not
|
|
* seen the event at all.
|
|
*
|
|
* @param keyCode The value in event.getKeyCode().
|
|
* @param count The number of times the action was made.
|
|
* @param event Description of the key event.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
*/
|
|
@Override
|
|
public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent)
|
|
* KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event).
|
|
* <p>
|
|
* Override this to intercept key up events before they are processed by the application. If
|
|
* you return true, the application will not itself process the event. If you return false,
|
|
* the normal application processing will occur as if the TV input had not seen the event at
|
|
* all.
|
|
*
|
|
* @param keyCode The value in event.getKeyCode().
|
|
* @param event Description of the key event.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
*/
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implement this method to handle touch screen motion events on the current input session.
|
|
*
|
|
* @param event The motion event being received.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
* @see View#onTouchEvent
|
|
*/
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implement this method to handle trackball events on the current input session.
|
|
*
|
|
* @param event The motion event being received.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
* @see View#onTrackballEvent
|
|
*/
|
|
public boolean onTrackballEvent(MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implement this method to handle generic motion events on the current input session.
|
|
*
|
|
* @param event The motion event being received.
|
|
* @return If you handled the event, return {@code true}. If you want to allow the event to
|
|
* be handled by the next receiver, return {@code false}.
|
|
* @see View#onGenericMotionEvent
|
|
*/
|
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This method is called when the application would like to stop using the current input
|
|
* session.
|
|
*/
|
|
void release() {
|
|
onRelease();
|
|
if (mSurface != null) {
|
|
mSurface.release();
|
|
mSurface = null;
|
|
}
|
|
synchronized(mLock) {
|
|
mSessionCallback = null;
|
|
mPendingActions.clear();
|
|
}
|
|
// Removes the overlay view lastly so that any hanging on the main thread can be handled
|
|
// in {@link #scheduleOverlayViewCleanup}.
|
|
removeOverlayView(true);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSetMain}.
|
|
*/
|
|
void setMain(boolean isMain) {
|
|
onSetMain(isMain);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSetSurface}.
|
|
*/
|
|
void setSurface(Surface surface) {
|
|
onSetSurface(surface);
|
|
if (mSurface != null) {
|
|
mSurface.release();
|
|
}
|
|
mSurface = surface;
|
|
// TODO: Handle failure.
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSurfaceChanged}.
|
|
*/
|
|
void dispatchSurfaceChanged(int format, int width, int height) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width
|
|
+ ", height=" + height + ")");
|
|
}
|
|
onSurfaceChanged(format, width, height);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSetStreamVolume}.
|
|
*/
|
|
void setStreamVolume(float volume) {
|
|
onSetStreamVolume(volume);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onTune}.
|
|
*/
|
|
void tune(Uri channelUri, Bundle params) {
|
|
onTune(channelUri, params);
|
|
// TODO: Handle failure.
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSetCaptionEnabled}.
|
|
*/
|
|
void setCaptionEnabled(boolean enabled) {
|
|
onSetCaptionEnabled(enabled);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onSelectTrack}.
|
|
*/
|
|
void selectTrack(int type, String trackId) {
|
|
onSelectTrack(type, trackId);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onUnblockContent}.
|
|
*/
|
|
void unblockContent(String unblockedRating) {
|
|
onUnblockContent(TvContentRating.unflattenFromString(unblockedRating));
|
|
// TODO: Handle failure.
|
|
}
|
|
|
|
/**
|
|
* Calls {@link #onAppPrivateCommand}.
|
|
*/
|
|
void appPrivateCommand(String action, Bundle data) {
|
|
onAppPrivateCommand(action, data);
|
|
}
|
|
|
|
/**
|
|
* Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach
|
|
* to the overlay window.
|
|
*
|
|
* @param windowToken A window token of an application.
|
|
* @param frame A position of the overlay view.
|
|
*/
|
|
void createOverlayView(IBinder windowToken, Rect frame) {
|
|
if (mOverlayViewContainer != null) {
|
|
removeOverlayView(false);
|
|
}
|
|
if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")");
|
|
mWindowToken = windowToken;
|
|
mOverlayFrame = frame;
|
|
onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
|
|
if (!mOverlayViewEnabled) {
|
|
return;
|
|
}
|
|
mOverlayView = onCreateOverlayView();
|
|
if (mOverlayView == null) {
|
|
return;
|
|
}
|
|
if (mOverlayViewCleanUpTask != null) {
|
|
mOverlayViewCleanUpTask.cancel(true);
|
|
mOverlayViewCleanUpTask = null;
|
|
}
|
|
// Creates a container view to check hanging on the overlay view detaching.
|
|
// Adding/removing the overlay view to/from the container make the view attach/detach
|
|
// logic run on the main thread.
|
|
mOverlayViewContainer = new FrameLayout(mContext);
|
|
mOverlayViewContainer.addView(mOverlayView);
|
|
// TvView's window type is TYPE_APPLICATION_MEDIA and we want to create
|
|
// an overlay window above the media window but below the application window.
|
|
int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
|
|
// We make the overlay view non-focusable and non-touchable so that
|
|
// the application that owns the window token can decide whether to consume or
|
|
// dispatch the input events.
|
|
int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
|
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
|
mWindowParams = new WindowManager.LayoutParams(
|
|
frame.right - frame.left, frame.bottom - frame.top,
|
|
frame.left, frame.top, type, flag, PixelFormat.TRANSPARENT);
|
|
mWindowParams.privateFlags |=
|
|
WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
|
|
mWindowParams.gravity = Gravity.START | Gravity.TOP;
|
|
mWindowParams.token = windowToken;
|
|
mWindowManager.addView(mOverlayViewContainer, mWindowParams);
|
|
}
|
|
|
|
/**
|
|
* Relayouts the current overlay view.
|
|
*
|
|
* @param frame A new position of the overlay view.
|
|
*/
|
|
void relayoutOverlayView(Rect frame) {
|
|
if (DEBUG) Log.d(TAG, "relayoutOverlayView(" + frame + ")");
|
|
if (mOverlayFrame == null || mOverlayFrame.width() != frame.width()
|
|
|| mOverlayFrame.height() != frame.height()) {
|
|
// Note: relayoutOverlayView is called whenever TvView's layout is changed
|
|
// regardless of setOverlayViewEnabled.
|
|
onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
|
|
}
|
|
mOverlayFrame = frame;
|
|
if (!mOverlayViewEnabled || mOverlayViewContainer == null) {
|
|
return;
|
|
}
|
|
mWindowParams.x = frame.left;
|
|
mWindowParams.y = frame.top;
|
|
mWindowParams.width = frame.right - frame.left;
|
|
mWindowParams.height = frame.bottom - frame.top;
|
|
mWindowManager.updateViewLayout(mOverlayViewContainer, mWindowParams);
|
|
}
|
|
|
|
/**
|
|
* Removes the current overlay view.
|
|
*/
|
|
void removeOverlayView(boolean clearWindowToken) {
|
|
if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayViewContainer + ")");
|
|
if (clearWindowToken) {
|
|
mWindowToken = null;
|
|
mOverlayFrame = null;
|
|
}
|
|
if (mOverlayViewContainer != null) {
|
|
// Removes the overlay view from the view hierarchy in advance so that it can be
|
|
// cleaned up in the {@link OverlayViewCleanUpTask} if the remove process is
|
|
// hanging.
|
|
mOverlayViewContainer.removeView(mOverlayView);
|
|
mOverlayView = null;
|
|
mWindowManager.removeView(mOverlayViewContainer);
|
|
mOverlayViewContainer = null;
|
|
mWindowParams = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedules a task which checks whether the overlay view is detached and kills the process
|
|
* if it is not. Note that this method is expected to be called in a non-main thread.
|
|
*/
|
|
void scheduleOverlayViewCleanup() {
|
|
View overlayViewParent = mOverlayViewContainer;
|
|
if (overlayViewParent != null) {
|
|
mOverlayViewCleanUpTask = new OverlayViewCleanUpTask();
|
|
mOverlayViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
|
overlayViewParent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes care of dispatching incoming input events and tells whether the event was handled.
|
|
*/
|
|
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
|
|
if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")");
|
|
boolean isNavigationKey = false;
|
|
if (event instanceof KeyEvent) {
|
|
KeyEvent keyEvent = (KeyEvent) event;
|
|
isNavigationKey = isNavigationKey(keyEvent.getKeyCode());
|
|
if (keyEvent.dispatch(this, mDispatcherState, this)) {
|
|
return TvInputManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
} else if (event instanceof MotionEvent) {
|
|
MotionEvent motionEvent = (MotionEvent) event;
|
|
final int source = motionEvent.getSource();
|
|
if (motionEvent.isTouchEvent()) {
|
|
if (onTouchEvent(motionEvent)) {
|
|
return TvInputManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
|
|
if (onTrackballEvent(motionEvent)) {
|
|
return TvInputManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
} else {
|
|
if (onGenericMotionEvent(motionEvent)) {
|
|
return TvInputManager.Session.DISPATCH_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
if (mOverlayViewContainer == null || !mOverlayViewContainer.isAttachedToWindow()) {
|
|
return TvInputManager.Session.DISPATCH_NOT_HANDLED;
|
|
}
|
|
if (!mOverlayViewContainer.hasWindowFocus()) {
|
|
mOverlayViewContainer.getViewRootImpl().windowFocusChanged(true, true);
|
|
}
|
|
if (isNavigationKey && mOverlayViewContainer.hasFocusable()) {
|
|
// If mOverlayView has focusable views, navigation key events should be always
|
|
// handled. If not, it can make the application UI navigation messed up.
|
|
// For example, in the case that the left-most view is focused, a left key event
|
|
// will not be handled in ViewRootImpl. Then, the left key event will be handled in
|
|
// the application during the UI navigation of the TV input.
|
|
mOverlayViewContainer.getViewRootImpl().dispatchInputEvent(event);
|
|
return TvInputManager.Session.DISPATCH_HANDLED;
|
|
} else {
|
|
mOverlayViewContainer.getViewRootImpl().dispatchInputEvent(event, receiver);
|
|
return TvInputManager.Session.DISPATCH_IN_PROGRESS;
|
|
}
|
|
}
|
|
|
|
private void initialize(ITvInputSessionCallback callback) {
|
|
synchronized(mLock) {
|
|
mSessionCallback = callback;
|
|
for (Runnable runnable : mPendingActions) {
|
|
runnable.run();
|
|
}
|
|
mPendingActions.clear();
|
|
}
|
|
}
|
|
|
|
private final void executeOrPostRunnable(Runnable action) {
|
|
synchronized(mLock) {
|
|
if (mSessionCallback == null) {
|
|
// The session is not initialized yet.
|
|
mPendingActions.add(action);
|
|
} else {
|
|
if (mHandler.getLooper().isCurrentThread()) {
|
|
action.run();
|
|
} else {
|
|
// Posts the runnable if this is not called from the main thread
|
|
mHandler.post(action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class OverlayViewCleanUpTask extends AsyncTask<View, Void, Void> {
|
|
@Override
|
|
protected Void doInBackground(View... views) {
|
|
View overlayViewParent = views[0];
|
|
try {
|
|
Thread.sleep(DETACH_OVERLAY_VIEW_TIMEOUT);
|
|
} catch (InterruptedException e) {
|
|
return null;
|
|
}
|
|
if (isCancelled()) {
|
|
return null;
|
|
}
|
|
if (overlayViewParent.isAttachedToWindow()) {
|
|
Log.e(TAG, "Time out on releasing overlay view. Killing "
|
|
+ overlayViewParent.getContext().getPackageName());
|
|
Process.killProcess(Process.myPid());
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for a TV input session which represents an external device connected to a
|
|
* hardware TV input.
|
|
* <p>
|
|
* This class is for an input which provides channels for the external set-top box to the
|
|
* application. Once a TV input returns an implementation of this class on
|
|
* {@link #onCreateSession(String)}, the framework will create a separate session for
|
|
* a hardware TV Input (e.g. HDMI 1) and forward the application's surface to the session so
|
|
* that the user can see the screen of the hardware TV Input when she tunes to a channel from
|
|
* this TV input. The implementation of this class is expected to change the channel of the
|
|
* external set-top box via a proprietary protocol when {@link HardwareSession#onTune(Uri)} is
|
|
* requested by the application.
|
|
* </p><p>
|
|
* Note that this class is not for inputs for internal hardware like built-in tuner and HDMI 1.
|
|
* </p>
|
|
* @see #onCreateSession(String)
|
|
*/
|
|
public abstract static class HardwareSession extends Session {
|
|
|
|
/**
|
|
* Creates a new HardwareSession.
|
|
*
|
|
* @param context The context of the application
|
|
*/
|
|
public HardwareSession(Context context) {
|
|
super(context);
|
|
}
|
|
|
|
private TvInputManager.Session mHardwareSession;
|
|
private ITvInputSession mProxySession;
|
|
private ITvInputSessionCallback mProxySessionCallback;
|
|
private Handler mServiceHandler;
|
|
|
|
/**
|
|
* Returns the hardware TV input ID the external device is connected to.
|
|
* <p>
|
|
* TV input is expected to provide {@link android.R.attr#setupActivity} so that
|
|
* the application can launch it before using this TV input. The setup activity may let
|
|
* the user select the hardware TV input to which the external device is connected. The ID
|
|
* of the selected one should be stored in the TV input so that it can be returned here.
|
|
* </p>
|
|
*/
|
|
public abstract String getHardwareInputId();
|
|
|
|
private final TvInputManager.SessionCallback mHardwareSessionCallback =
|
|
new TvInputManager.SessionCallback() {
|
|
@Override
|
|
public void onSessionCreated(TvInputManager.Session session) {
|
|
mHardwareSession = session;
|
|
SomeArgs args = SomeArgs.obtain();
|
|
if (session != null) {
|
|
args.arg1 = HardwareSession.this;
|
|
args.arg2 = mProxySession;
|
|
args.arg3 = mProxySessionCallback;
|
|
args.arg4 = session.getToken();
|
|
} else {
|
|
args.arg1 = null;
|
|
args.arg2 = null;
|
|
args.arg3 = mProxySessionCallback;
|
|
args.arg4 = null;
|
|
onRelease();
|
|
}
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED, args)
|
|
.sendToTarget();
|
|
}
|
|
|
|
@Override
|
|
public void onVideoAvailable(final TvInputManager.Session session) {
|
|
if (mHardwareSession == session) {
|
|
onHardwareVideoAvailable();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onVideoUnavailable(final TvInputManager.Session session,
|
|
final int reason) {
|
|
if (mHardwareSession == session) {
|
|
onHardwareVideoUnavailable(reason);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This method will not be called in {@link HardwareSession}. Framework will
|
|
* forward the application's surface to the hardware TV input.
|
|
*/
|
|
@Override
|
|
public final boolean onSetSurface(Surface surface) {
|
|
Log.e(TAG, "onSetSurface() should not be called in HardwareProxySession.");
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called when the underlying hardware TV input session calls
|
|
* {@link TvInputService.Session#notifyVideoAvailable()}.
|
|
*/
|
|
public void onHardwareVideoAvailable() { }
|
|
|
|
/**
|
|
* Called when the underlying hardware TV input session calls
|
|
* {@link TvInputService.Session#notifyVideoUnavailable(int)}.
|
|
*
|
|
* @param reason The reason that the hardware TV input stopped the playback:
|
|
* <ul>
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
|
|
* <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
|
|
* </ul>
|
|
*/
|
|
public void onHardwareVideoUnavailable(int reason) { }
|
|
}
|
|
|
|
/** @hide */
|
|
public static boolean isNavigationKey(int keyCode) {
|
|
switch (keyCode) {
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
case KeyEvent.KEYCODE_DPAD_UP:
|
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
|
case KeyEvent.KEYCODE_PAGE_UP:
|
|
case KeyEvent.KEYCODE_PAGE_DOWN:
|
|
case KeyEvent.KEYCODE_MOVE_HOME:
|
|
case KeyEvent.KEYCODE_MOVE_END:
|
|
case KeyEvent.KEYCODE_TAB:
|
|
case KeyEvent.KEYCODE_SPACE:
|
|
case KeyEvent.KEYCODE_ENTER:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@SuppressLint("HandlerLeak")
|
|
private final class ServiceHandler extends Handler {
|
|
private static final int DO_CREATE_SESSION = 1;
|
|
private static final int DO_NOTIFY_SESSION_CREATED = 2;
|
|
private static final int DO_ADD_HARDWARE_TV_INPUT = 3;
|
|
private static final int DO_REMOVE_HARDWARE_TV_INPUT = 4;
|
|
private static final int DO_ADD_HDMI_TV_INPUT = 5;
|
|
private static final int DO_REMOVE_HDMI_TV_INPUT = 6;
|
|
|
|
private void broadcastAddHardwareTvInput(int deviceId, TvInputInfo inputInfo) {
|
|
int n = mCallbacks.beginBroadcast();
|
|
for (int i = 0; i < n; ++i) {
|
|
try {
|
|
mCallbacks.getBroadcastItem(i).addHardwareTvInput(deviceId, inputInfo);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error while broadcasting.", e);
|
|
}
|
|
}
|
|
mCallbacks.finishBroadcast();
|
|
}
|
|
|
|
private void broadcastAddHdmiTvInput(int id, TvInputInfo inputInfo) {
|
|
int n = mCallbacks.beginBroadcast();
|
|
for (int i = 0; i < n; ++i) {
|
|
try {
|
|
mCallbacks.getBroadcastItem(i).addHdmiTvInput(id, inputInfo);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error while broadcasting.", e);
|
|
}
|
|
}
|
|
mCallbacks.finishBroadcast();
|
|
}
|
|
|
|
private void broadcastRemoveTvInput(String inputId) {
|
|
int n = mCallbacks.beginBroadcast();
|
|
for (int i = 0; i < n; ++i) {
|
|
try {
|
|
mCallbacks.getBroadcastItem(i).removeTvInput(inputId);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error while broadcasting.", e);
|
|
}
|
|
}
|
|
mCallbacks.finishBroadcast();
|
|
}
|
|
|
|
@Override
|
|
public final void handleMessage(Message msg) {
|
|
switch (msg.what) {
|
|
case DO_CREATE_SESSION: {
|
|
SomeArgs args = (SomeArgs) msg.obj;
|
|
InputChannel channel = (InputChannel) args.arg1;
|
|
ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2;
|
|
String inputId = (String) args.arg3;
|
|
args.recycle();
|
|
Session sessionImpl = onCreateSession(inputId);
|
|
if (sessionImpl == null) {
|
|
try {
|
|
// Failed to create a session.
|
|
cb.onSessionCreated(null, null);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "error in onSessionCreated");
|
|
}
|
|
return;
|
|
}
|
|
ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this,
|
|
sessionImpl, channel);
|
|
if (sessionImpl instanceof HardwareSession) {
|
|
HardwareSession proxySession =
|
|
((HardwareSession) sessionImpl);
|
|
String harewareInputId = proxySession.getHardwareInputId();
|
|
if (TextUtils.isEmpty(harewareInputId) ||
|
|
!isPassthroughInput(harewareInputId)) {
|
|
if (TextUtils.isEmpty(harewareInputId)) {
|
|
Log.w(TAG, "Hardware input id is not setup yet.");
|
|
} else {
|
|
Log.w(TAG, "Invalid hardware input id : " + harewareInputId);
|
|
}
|
|
sessionImpl.onRelease();
|
|
try {
|
|
cb.onSessionCreated(null, null);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "error in onSessionCreated");
|
|
}
|
|
return;
|
|
}
|
|
proxySession.mProxySession = stub;
|
|
proxySession.mProxySessionCallback = cb;
|
|
proxySession.mServiceHandler = mServiceHandler;
|
|
TvInputManager manager = (TvInputManager) getSystemService(
|
|
Context.TV_INPUT_SERVICE);
|
|
manager.createSession(harewareInputId,
|
|
proxySession.mHardwareSessionCallback, mServiceHandler);
|
|
} else {
|
|
SomeArgs someArgs = SomeArgs.obtain();
|
|
someArgs.arg1 = sessionImpl;
|
|
someArgs.arg2 = stub;
|
|
someArgs.arg3 = cb;
|
|
someArgs.arg4 = null;
|
|
mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED,
|
|
someArgs).sendToTarget();
|
|
}
|
|
return;
|
|
}
|
|
case DO_NOTIFY_SESSION_CREATED: {
|
|
SomeArgs args = (SomeArgs) msg.obj;
|
|
Session sessionImpl = (Session) args.arg1;
|
|
ITvInputSession stub = (ITvInputSession) args.arg2;
|
|
ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg3;
|
|
IBinder hardwareSessionToken = (IBinder) args.arg4;
|
|
try {
|
|
cb.onSessionCreated(stub, hardwareSessionToken);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "error in onSessionCreated");
|
|
}
|
|
if (sessionImpl != null) {
|
|
sessionImpl.initialize(cb);
|
|
}
|
|
args.recycle();
|
|
return;
|
|
}
|
|
case DO_ADD_HARDWARE_TV_INPUT: {
|
|
TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj;
|
|
TvInputInfo inputInfo = onHardwareAdded(hardwareInfo);
|
|
if (inputInfo != null) {
|
|
broadcastAddHardwareTvInput(hardwareInfo.getDeviceId(), inputInfo);
|
|
}
|
|
return;
|
|
}
|
|
case DO_REMOVE_HARDWARE_TV_INPUT: {
|
|
TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj;
|
|
String inputId = onHardwareRemoved(hardwareInfo);
|
|
if (inputId != null) {
|
|
broadcastRemoveTvInput(inputId);
|
|
}
|
|
return;
|
|
}
|
|
case DO_ADD_HDMI_TV_INPUT: {
|
|
HdmiDeviceInfo deviceInfo = (HdmiDeviceInfo) msg.obj;
|
|
TvInputInfo inputInfo = onHdmiDeviceAdded(deviceInfo);
|
|
if (inputInfo != null) {
|
|
broadcastAddHdmiTvInput(deviceInfo.getId(), inputInfo);
|
|
}
|
|
return;
|
|
}
|
|
case DO_REMOVE_HDMI_TV_INPUT: {
|
|
HdmiDeviceInfo deviceInfo = (HdmiDeviceInfo) msg.obj;
|
|
String inputId = onHdmiDeviceRemoved(deviceInfo);
|
|
if (inputId != null) {
|
|
broadcastRemoveTvInput(inputId);
|
|
}
|
|
return;
|
|
}
|
|
default: {
|
|
Log.w(TAG, "Unhandled message code: " + msg.what);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|