blob: 409a33c7a25b1f645ae517f8635778b82862e168 [file] [log] [blame]
/*
* 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>&lt;{@link android.R.styleable#TvInputService tv-input}&gt;</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;
}
}
}
}
}