/* * 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. *
* 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. *
*/ 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 *<{@link android.R.styleable#TvInputService tv-input}>
* 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* 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. *
* @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* 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. *
* 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. *
* * @see #notifyContentBlocked * @see TvInputManager */ public void notifyContentAllowed() { executeOrPostRunnable(new Runnable() { @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyContentAllowed"); if (mSessionCallback != null) { mSessionCallback.onContentAllowed(); } } catch (RemoteException e) { Log.w(TAG, "error in notifyContentAllowed"); } } }); } /** * Informs the application that the current program content is blocked by parent controls. ** 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. *
* 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. *
* * @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"); if (mSessionCallback != null) { 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 + ",)"); if (mSessionCallback != null) { 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. ** 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. *
* {@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. *
* * @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. ** 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. *
* 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. *
* * @param unblockedRating An unblocked content rating */ public void onUnblockContent(TvContentRating unblockedRating) { } /** * Select a given track. ** If this is done successfully, the implementation should call {@link #notifyTrackSelected} * to help applications maintain the selcted track lists. *
* * @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 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. * @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). ** 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). *
* 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). *
* 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). *
* 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
* 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.
*
* Note that this class is not for inputs for internal hardware like built-in tuner and HDMI 1.
*
* 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.
*
*
*/
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;
}
}
}
}
}