| /* |
| * 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.app.Service; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| 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 com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.SomeArgs; |
| |
| /** |
| * 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 { |
| // STOPSHIP: Turn debugging off. |
| private static final boolean DEBUG = true; |
| 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"; |
| |
| private String mId; |
| private final Handler mHandler = new ServiceHandler(); |
| private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks = |
| new RemoteCallbackList<ITvInputServiceCallback>(); |
| // STOPSHIP: Redesign the API around the availability change. For now, the service will be |
| // always available. |
| private final boolean mAvailable = true; |
| |
| @Override |
| public void onCreate() { |
| super.onCreate(); |
| mId = TvInputInfo.generateInputIdForComponentName( |
| new ComponentName(getPackageName(), getClass().getName())); |
| } |
| |
| @Override |
| public final IBinder onBind(Intent intent) { |
| return new ITvInputService.Stub() { |
| @Override |
| public void registerCallback(ITvInputServiceCallback cb) { |
| if (cb != null) { |
| mCallbacks.register(cb); |
| // The first time a callback is registered, the service needs to report its |
| // availability status so that the system can know its initial value. |
| try { |
| cb.onAvailabilityChanged(mId, mAvailable); |
| } catch (RemoteException e) { |
| Log.e(TAG, "error in onAvailabilityChanged", e); |
| } |
| } |
| } |
| |
| @Override |
| public void unregisterCallback(ITvInputServiceCallback cb) { |
| if (cb != null) { |
| mCallbacks.unregister(cb); |
| } |
| } |
| |
| @Override |
| public void createSession(InputChannel channel, ITvInputSessionCallback cb) { |
| 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; |
| mHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget(); |
| } |
| }; |
| } |
| |
| /** |
| * Get the number of callbacks that are registered. |
| * |
| * @hide |
| */ |
| @VisibleForTesting |
| public final int getRegisteredCallbackCount() { |
| return mCallbacks.getRegisteredCallbackCount(); |
| } |
| |
| /** |
| * 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. |
| * </p> |
| */ |
| public abstract Session onCreateSession(); |
| |
| /** |
| * Base class for derived classes to implement to provide a TV input session. |
| */ |
| public abstract class Session implements KeyEvent.Callback { |
| private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); |
| private final WindowManager mWindowManager; |
| private WindowManager.LayoutParams mWindowParams; |
| private Surface mSurface; |
| private View mOverlayView; |
| private boolean mOverlayViewEnabled; |
| private IBinder mWindowToken; |
| private Rect mOverlayFrame; |
| private ITvInputSessionCallback mSessionCallback; |
| |
| public Session() { |
| mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); |
| } |
| |
| /** |
| * 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 |
| */ |
| public void dispatchSessionEvent(final String eventType, final Bundle eventArgs) { |
| if (eventType == null) { |
| throw new IllegalArgumentException("eventType should not be null."); |
| } |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| if (DEBUG) Log.d(TAG, "dispatchSessionEvent(" + eventType + ")"); |
| mSessionCallback.onSessionEvent(eventType, eventArgs); |
| } catch (RemoteException e) { |
| Log.w(TAG, "error in sending event (event=" + eventType + ")"); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Sends the change on the format of the video stream. This is expected to be called at the |
| * beginning of the playback and later when the format has been changed. |
| * |
| * @param width The width of the video. |
| * @param height The height of the video. |
| * @param interlaced Whether the video is interlaced mode or planer mode. |
| * @hide |
| */ |
| public void dispatchVideoStreamChanged(final int width, final int height, |
| final boolean interlaced) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| if (DEBUG) Log.d(TAG, "dispatchVideoSizeChanged"); |
| mSessionCallback.onVideoStreamChanged(width, height, interlaced); |
| } catch (RemoteException e) { |
| Log.w(TAG, "error in dispatchVideoSizeChanged"); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Sends the change on the format of the audio stream. This is expected to be called at the |
| * beginning of the playback and later when the format has been changed. |
| * |
| * @param channelNumber The number of channels in the audio stream. |
| * @hide |
| */ |
| public void dispatchAudioStreamChanged(final int channelNumber) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| if (DEBUG) Log.d(TAG, "dispatchAudioStreamChanged"); |
| mSessionCallback.onAudioStreamChanged(channelNumber); |
| } catch (RemoteException e) { |
| Log.w(TAG, "error in dispatchAudioStreamChanged"); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Sends the change on the closed caption stream. This is expected to be called at the |
| * beginning of the playback and later when the stream has been changed. |
| * |
| * @param hasClosedCaption Whether the stream has closed caption or not. |
| * @hide |
| */ |
| public void dispatchClosedCaptionStreamChanged(final boolean hasClosedCaption) { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| if (DEBUG) Log.d(TAG, "dispatchClosedCaptionStreamChanged"); |
| mSessionCallback.onClosedCaptionStreamChanged(hasClosedCaption); |
| } catch (RemoteException e) { |
| Log.w(TAG, "error in dispatchClosedCaptionStreamChanged"); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Called when the session is released. |
| */ |
| public abstract void onRelease(); |
| |
| /** |
| * 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); |
| |
| /** |
| * 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. |
| * |
| * @param channelUri The URI of the channel. |
| * @return {@code true} the tuning was successful, {@code false} otherwise. |
| */ |
| public abstract boolean onTune(Uri channelUri); |
| |
| /** |
| * 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; |
| } |
| removeOverlayView(true); |
| } |
| |
| /** |
| * Calls {@link #onSetSurface}. |
| */ |
| void setSurface(Surface surface) { |
| onSetSurface(surface); |
| if (mSurface != null) { |
| mSurface.release(); |
| } |
| mSurface = surface; |
| // TODO: Handle failure. |
| } |
| |
| /** |
| * Calls {@link #onSetStreamVolume}. |
| */ |
| void setVolume(float volume) { |
| onSetStreamVolume(volume); |
| } |
| |
| /** |
| * Calls {@link #onTune}. |
| */ |
| void tune(Uri channelUri) { |
| onTune(channelUri); |
| // TODO: Handle failure. |
| } |
| |
| /** |
| * 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 (mOverlayView != null) { |
| mWindowManager.removeView(mOverlayView); |
| mOverlayView = null; |
| } |
| if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")"); |
| mWindowToken = windowToken; |
| mOverlayFrame = frame; |
| if (!mOverlayViewEnabled) { |
| return; |
| } |
| mOverlayView = onCreateOverlayView(); |
| if (mOverlayView == null) { |
| return; |
| } |
| // 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_IN_SCREEN |
| | 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(mOverlayView, 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 + ")"); |
| mOverlayFrame = frame; |
| if (!mOverlayViewEnabled || mOverlayView == null) { |
| return; |
| } |
| mWindowParams.x = frame.left; |
| mWindowParams.y = frame.top; |
| mWindowParams.width = frame.right - frame.left; |
| mWindowParams.height = frame.bottom - frame.top; |
| mWindowManager.updateViewLayout(mOverlayView, mWindowParams); |
| } |
| |
| /** |
| * Removes the current overlay view. |
| */ |
| void removeOverlayView(boolean clearWindowToken) { |
| if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayView + ")"); |
| if (clearWindowToken) { |
| mWindowToken = null; |
| mOverlayFrame = null; |
| } |
| if (mOverlayView != null) { |
| mWindowManager.removeView(mOverlayView); |
| mOverlayView = null; |
| mWindowParams = null; |
| } |
| } |
| |
| /** |
| * 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 + ")"); |
| if (event instanceof KeyEvent) { |
| if (((KeyEvent) event).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 (mOverlayView == null || !mOverlayView.isAttachedToWindow()) { |
| return TvInputManager.Session.DISPATCH_NOT_HANDLED; |
| } |
| if (!mOverlayView.hasWindowFocus()) { |
| mOverlayView.getViewRootImpl().windowFocusChanged(true, true); |
| } |
| mOverlayView.getViewRootImpl().dispatchInputEvent(event, receiver); |
| return TvInputManager.Session.DISPATCH_IN_PROGRESS; |
| } |
| |
| private void setSessionCallback(ITvInputSessionCallback callback) { |
| mSessionCallback = callback; |
| } |
| } |
| |
| private final class ServiceHandler extends Handler { |
| private static final int DO_CREATE_SESSION = 1; |
| private static final int DO_BROADCAST_AVAILABILITY_CHANGE = 2; |
| |
| @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; |
| try { |
| Session sessionImpl = onCreateSession(); |
| if (sessionImpl == null) { |
| // Failed to create a session. |
| cb.onSessionCreated(null); |
| } else { |
| sessionImpl.setSessionCallback(cb); |
| ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, |
| sessionImpl, channel); |
| cb.onSessionCreated(stub); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "error in onSessionCreated"); |
| } |
| args.recycle(); |
| return; |
| } |
| case DO_BROADCAST_AVAILABILITY_CHANGE: { |
| boolean isAvailable = (Boolean) msg.obj; |
| int n = mCallbacks.beginBroadcast(); |
| try { |
| for (int i = 0; i < n; i++) { |
| mCallbacks.getBroadcastItem(i).onAvailabilityChanged(mId, isAvailable); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unexpected exception", e); |
| } finally { |
| mCallbacks.finishBroadcast(); |
| } |
| return; |
| } |
| default: { |
| Log.w(TAG, "Unhandled message code: " + msg.what); |
| return; |
| } |
| } |
| } |
| } |
| } |