/* * 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 mCallbacks = new RemoteCallbackList(); 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}. *

* 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 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 + ")"); if (mSessionCallback != null) { 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"); if (mSessionCallback != null) { 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 tracks) { Set trackIdSet = new HashSet(); 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"); if (mSessionCallback != null) { 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"); if (mSessionCallback != null) { 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"); if (mSessionCallback != null) { 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: *
    *
  • {@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} *
* @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"); if (mSessionCallback != null) { 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. *

* 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 { @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. *

* 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. *

* @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. *

* 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 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(); session.tune(TvContract.buildChannelUriForPassthroughInput(getHardwareInputId())); } @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: *
    *
  • {@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} *
*/ 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; } } } } }