diff options
6 files changed, 442 insertions, 2 deletions
diff --git a/media/java/android/media/tv/interactive/ITvIAppManager.aidl b/media/java/android/media/tv/interactive/ITvIAppManager.aidl index 25e1acea226d..2027626893c0 100644 --- a/media/java/android/media/tv/interactive/ITvIAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppManager.aidl @@ -16,6 +16,7 @@ package android.media.tv.interactive; +import android.graphics.Rect; import android.media.tv.interactive.ITvIAppClient; import android.media.tv.interactive.ITvIAppManagerCallback; import android.media.tv.interactive.TvIAppInfo; @@ -38,6 +39,11 @@ interface ITvIAppManager { void notifyBroadcastInfoResponse(in IBinder sessionToken, in BroadcastInfoResponse response, int UserId); + void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, + int userId); + void relayoutMediaView(in IBinder sessionToken, in Rect frame, int userId); + void removeMediaView(in IBinder sessionToken, int userId); + void registerCallback(in ITvIAppManagerCallback callback, int userId); void unregisterCallback(in ITvIAppManagerCallback callback, int userId); }
\ No newline at end of file diff --git a/media/java/android/media/tv/interactive/ITvIAppSession.aidl b/media/java/android/media/tv/interactive/ITvIAppSession.aidl index 440b3d30c068..ef35c68beb6f 100644 --- a/media/java/android/media/tv/interactive/ITvIAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppSession.aidl @@ -16,6 +16,7 @@ package android.media.tv.interactive; +import android.graphics.Rect; import android.view.Surface; import android.media.tv.BroadcastInfoResponse; @@ -29,4 +30,8 @@ oneway interface ITvIAppSession { void setSurface(in Surface surface); void dispatchSurfaceChanged(int format, int width, int height); void notifyBroadcastInfoResponse(in BroadcastInfoResponse response); + + void createMediaView(in IBinder windowToken, in Rect frame); + void relayoutMediaView(in Rect frame); + void removeMediaView(); }
\ No newline at end of file diff --git a/media/java/android/media/tv/interactive/TvIAppManager.java b/media/java/android/media/tv/interactive/TvIAppManager.java index ae35edc7ef8a..5c7f0b9fe8a2 100644 --- a/media/java/android/media/tv/interactive/TvIAppManager.java +++ b/media/java/android/media/tv/interactive/TvIAppManager.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; +import android.graphics.Rect; import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvInputManager; @@ -35,6 +36,7 @@ import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventSender; import android.view.Surface; +import android.view.View; import com.android.internal.util.Preconditions; @@ -443,6 +445,67 @@ public final class TvIAppManager { } /** + * Creates a media view. Once the media view is created, {@link #relayoutMediaView} + * should be called whenever the layout of its containing view is changed. + * {@link #removeMediaView()} should be called to remove the media view. + * Since a session can have only one media view, this method should be called only once + * or it can be called again after calling {@link #removeMediaView()}. + * + * @param view A view for interactive app. + * @param frame A position of the media view. + * @throws IllegalStateException if {@code view} is not attached to a window. + */ + void createMediaView(@NonNull View view, @NonNull Rect frame) { + Preconditions.checkNotNull(view); + Preconditions.checkNotNull(frame); + if (view.getWindowToken() == null) { + throw new IllegalStateException("view must be attached to a window"); + } + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.createMediaView(mToken, view.getWindowToken(), frame, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Relayouts the current media view. + * + * @param frame A new position of the media view. + */ + void relayoutMediaView(@NonNull Rect frame) { + Preconditions.checkNotNull(frame); + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.relayoutMediaView(mToken, frame, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes the current media view. + */ + void removeMediaView() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.removeMediaView(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Notifies of any structural changes (format or size) of the surface passed in * {@link #setSurface}. * diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java index fe087ca564d0..7b30f6e0d0af 100644 --- a/media/java/android/media/tv/interactive/TvIAppService.java +++ b/media/java/android/media/tv/interactive/TvIAppService.java @@ -20,18 +20,24 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.Rect; import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; +import android.os.AsyncTask; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.Process; 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; @@ -39,6 +45,9 @@ 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.widget.FrameLayout; import com.android.internal.os.SomeArgs; @@ -52,6 +61,8 @@ public abstract class TvIAppService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvIAppService"; + private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000; + // TODO: cleanup and unhide APIs. /** @@ -141,8 +152,16 @@ public abstract class TvIAppService extends Service { private final List<Runnable> mPendingActions = new ArrayList<>(); private final Context mContext; - private final Handler mHandler; + final Handler mHandler; + private final WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; private Surface mSurface; + private FrameLayout mMediaViewContainer; + private View mMediaView; + private MediaViewCleanUpTask mMediaViewCleanUpTask; + private boolean mMediaViewEnabled; + private IBinder mWindowToken; + private Rect mMediaFrame; /** * Creates a new Session. @@ -151,10 +170,41 @@ public abstract class TvIAppService extends Service { */ public Session(Context context) { mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mHandler = new Handler(context.getMainLooper()); } /** + * Enables or disables the media view. + * + * <p>By default, the media view is disabled. Must be called explicitly after the + * session is created to enable the media view. + * + * <p>The TV IApp service can disable its media view when needed. + * + * @param enable {@code true} if you want to enable the media view. {@code false} + * otherwise. + */ + public void setMediaViewEnabled(final boolean enable) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (enable == mMediaViewEnabled) { + return; + } + mMediaViewEnabled = enable; + if (enable) { + if (mWindowToken != null) { + createMediaView(mWindowToken, mMediaFrame); + } + } else { + removeMediaView(false); + } + } + }); + } + + /** * Starts TvIAppService session. * @hide */ @@ -195,6 +245,30 @@ public abstract class TvIAppService extends Service { } /** + * Called when the size of the media view is changed by the application. + * + * <p>This is always called at least once when the session is created regardless of whether + * the media view is enabled or not. The media view container size is the same as the + * containing {@link TvIAppView}. Note that the size of the underlying surface can be + * different if the surface was changed by calling {@link #layoutSurface}. + * + * @param width The width of the media view. + * @param height The height of the media view. + */ + public void onMediaViewSizeChanged(int width, int height) { + } + + /** + * Called when the application requests to create an media view. Each session + * implementation can override this method and return its own view. + * + * @return a view attached to the media window + */ + public View onCreateMediaView() { + return null; + } + + /** * Releases TvIAppService session. * @hide */ @@ -318,6 +392,13 @@ public abstract class TvIAppService extends Service { mSurface.release(); mSurface = null; } + synchronized (mLock) { + mSessionCallback = null; + mPendingActions.clear(); + } + // Removes the media view lastly so that any hanging on the main thread can be handled + // in {@link #scheduleMediaViewCleanup}. + removeMediaView(true); } /** @@ -413,6 +494,137 @@ public abstract class TvIAppService extends Service { } } } + + /** + * Creates an media view. This calls {@link #onCreateMediaView} to get a view to attach + * to the media window. + * + * @param windowToken A window token of the application. + * @param frame A position of the media view. + */ + void createMediaView(IBinder windowToken, Rect frame) { + if (mMediaViewContainer != null) { + removeMediaView(false); + } + if (DEBUG) Log.d(TAG, "create media view(" + frame + ")"); + mWindowToken = windowToken; + mMediaFrame = frame; + onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); + if (!mMediaViewEnabled) { + return; + } + mMediaView = onCreateMediaView(); + if (mMediaView == null) { + return; + } + if (mMediaViewCleanUpTask != null) { + mMediaViewCleanUpTask.cancel(true); + mMediaViewCleanUpTask = null; + } + // Creates a container view to check hanging on the media view detaching. + // Adding/removing the media view to/from the container make the view attach/detach + // logic run on the main thread. + mMediaViewContainer = new FrameLayout(mContext.getApplicationContext()); + mMediaViewContainer.addView(mMediaView); + + int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; + // 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 flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + if (ActivityManager.isHighEndGfx()) { + flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + mWindowParams = new WindowManager.LayoutParams( + frame.right - frame.left, frame.bottom - frame.top, + frame.left, frame.top, type, flags, PixelFormat.TRANSPARENT); + mWindowParams.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mWindowParams.gravity = Gravity.START | Gravity.TOP; + mWindowParams.token = windowToken; + mWindowManager.addView(mMediaViewContainer, mWindowParams); + } + + /** + * Relayouts the current media view. + * + * @param frame A new position of the media view. + */ + void relayoutMediaView(Rect frame) { + if (DEBUG) Log.d(TAG, "relayoutMediaView(" + frame + ")"); + if (mMediaFrame == null || mMediaFrame.width() != frame.width() + || mMediaFrame.height() != frame.height()) { + // Note: relayoutMediaView is called whenever TvIAppView's layout is changed + // regardless of setMediaViewEnabled. + onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); + } + mMediaFrame = frame; + if (!mMediaViewEnabled || mMediaViewContainer == null) { + return; + } + mWindowParams.x = frame.left; + mWindowParams.y = frame.top; + mWindowParams.width = frame.right - frame.left; + mWindowParams.height = frame.bottom - frame.top; + mWindowManager.updateViewLayout(mMediaViewContainer, mWindowParams); + } + + /** + * Removes the current media view. + */ + void removeMediaView(boolean clearWindowToken) { + if (DEBUG) Log.d(TAG, "removeMediaView(" + mMediaViewContainer + ")"); + if (clearWindowToken) { + mWindowToken = null; + mMediaFrame = null; + } + if (mMediaViewContainer != null) { + // Removes the media view from the view hierarchy in advance so that it can be + // cleaned up in the {@link MediaViewCleanUpTask} if the remove process is + // hanging. + mMediaViewContainer.removeView(mMediaView); + mMediaView = null; + mWindowManager.removeView(mMediaViewContainer); + mMediaViewContainer = null; + mWindowParams = null; + } + } + + /** + * Schedules a task which checks whether the media 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 scheduleMediaViewCleanup() { + View mediaViewParent = mMediaViewContainer; + if (mediaViewParent != null) { + mMediaViewCleanUpTask = new MediaViewCleanUpTask(); + mMediaViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + mediaViewParent); + } + } + } + + private static final class MediaViewCleanUpTask extends AsyncTask<View, Void, Void> { + @Override + protected Void doInBackground(View... views) { + View mediaViewParent = views[0]; + try { + Thread.sleep(DETACH_MEDIA_VIEW_TIMEOUT_MS); + } catch (InterruptedException e) { + return null; + } + if (isCancelled()) { + return null; + } + if (mediaViewParent.isAttachedToWindow()) { + Log.e(TAG, "Time out on releasing media view. Killing " + + mediaViewParent.getContext().getPackageName()); + android.os.Process.killProcess(Process.myPid()); + } + return null; + } } /** @@ -440,6 +652,7 @@ public abstract class TvIAppService extends Service { @Override public void release() { + mSessionImpl.scheduleMediaViewCleanup(); mSessionImpl.release(); } @@ -458,6 +671,21 @@ public abstract class TvIAppService extends Service { mSessionImpl.notifyBroadcastInfoResponse(response); } + @Override + public void createMediaView(IBinder windowToken, Rect frame) { + mSessionImpl.createMediaView(windowToken, frame); + } + + @Override + public void relayoutMediaView(Rect frame) { + mSessionImpl.relayoutMediaView(frame); + } + + @Override + public void removeMediaView() { + mSessionImpl.removeMediaView(true); + } + private final class TvIAppEventReceiver extends InputEventReceiver { TvIAppEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/media/java/android/media/tv/interactive/TvIAppView.java b/media/java/android/media/tv/interactive/TvIAppView.java index 1b25c23deea0..803198162d30 100644 --- a/media/java/android/media/tv/interactive/TvIAppView.java +++ b/media/java/android/media/tv/interactive/TvIAppView.java @@ -20,6 +20,8 @@ import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; +import android.graphics.Rect; +import android.graphics.RectF; import android.media.tv.TvInputManager; import android.media.tv.TvView; import android.media.tv.interactive.TvIAppManager.Session; @@ -65,6 +67,9 @@ public class TvIAppView extends ViewGroup { private int mSurfaceViewTop; private int mSurfaceViewBottom; + private boolean mMediaViewCreated; + private Rect mMediaViewFrame; + private final AttributeSet mAttrs; private final int mDefStyleAttr; private final XmlResourceParser mParser; @@ -119,6 +124,18 @@ public class TvIAppView extends ViewGroup { } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + createSessionMediaView(); + } + + @Override + protected void onDetachedFromWindow() { + removeSessionMediaView(); + super.onDetachedFromWindow(); + } + + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (DEBUG) { Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right @@ -147,6 +164,11 @@ public class TvIAppView extends ViewGroup { protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); mSurfaceView.setVisibility(visibility); + if (visibility == View.VISIBLE) { + createSessionMediaView(); + } else { + removeSessionMediaView(); + } } private void resetSurfaceView() { @@ -155,7 +177,12 @@ public class TvIAppView extends ViewGroup { removeView(mSurfaceView); } mSurface = null; - mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr); + mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) { + @Override + protected void updateSurface() { + super.updateSurface(); + relayoutSessionMediaView(); + }}; // The surface view's content should be treated as secure all the time. mSurfaceView.setSecure(true); mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); @@ -170,6 +197,46 @@ public class TvIAppView extends ViewGroup { resetInternal(); } + private void createSessionMediaView() { + // TODO: handle z-order + if (mSession == null || !isAttachedToWindow() || mMediaViewCreated) { + return; + } + mMediaViewFrame = getViewFrameOnScreen(); + mSession.createMediaView(this, mMediaViewFrame); + mMediaViewCreated = true; + } + + private void removeSessionMediaView() { + if (mSession == null || !mMediaViewCreated) { + return; + } + mSession.removeMediaView(); + mMediaViewCreated = false; + mMediaViewFrame = null; + } + + private void relayoutSessionMediaView() { + if (mSession == null || !isAttachedToWindow() || !mMediaViewCreated) { + return; + } + Rect viewFrame = getViewFrameOnScreen(); + if (viewFrame.equals(mMediaViewFrame)) { + return; + } + mSession.relayoutMediaView(viewFrame); + mMediaViewFrame = viewFrame; + } + + private Rect getViewFrameOnScreen() { + Rect frame = new Rect(); + getGlobalVisibleRect(frame); + RectF frameF = new RectF(frame); + getMatrix().mapRect(frameF); + frameF.round(frame); + return frame; + } + private void setSessionSurface(Surface surface) { if (mSession == null) { return; @@ -214,6 +281,7 @@ public class TvIAppView extends ViewGroup { mSessionCallback = null; if (mSession != null) { setSessionSurface(null); + removeSessionMediaView(); mUseRequestedSurfaceLayout = false; mSession.release(); mSession = null; @@ -287,6 +355,7 @@ public class TvIAppView extends ViewGroup { dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); } } + createSessionMediaView(); } else { // Failed to create // Todo: forward error to Tv App @@ -303,6 +372,8 @@ public class TvIAppView extends ViewGroup { Log.w(TAG, "onSessionReleased - session not created"); return; } + mMediaViewCreated = false; + mMediaViewFrame = null; mSessionCallback = null; mSession = null; } diff --git a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java index c7b6421cd0c9..cd757d2a626e 100644 --- a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java @@ -28,6 +28,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; +import android.graphics.Rect; import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.interactive.ITvIAppClient; @@ -570,6 +571,11 @@ public class TvIAppManagerService extends SystemService { } @GuardedBy("mLock") + private ITvIAppSession getSessionLocked(IBinder sessionToken, int callingUid, int userId) { + return getSessionLocked(getSessionStateLocked(sessionToken, callingUid, userId)); + } + + @GuardedBy("mLock") private ITvIAppSession getSessionLocked(SessionState sessionState) { ITvIAppSession session = sessionState.mSession; if (session == null) { @@ -802,6 +808,67 @@ public class TvIAppManagerService extends SystemService { Binder.restoreCallingIdentity(identity); } } + + @Override + public void createMediaView(IBinder sessionToken, IBinder windowToken, Rect frame, + int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "createMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId) + .createMediaView(windowToken, frame); + } catch (RemoteException | TvIAppManagerService.SessionNotFoundException e) { + Slog.e(TAG, "error in createMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void relayoutMediaView(IBinder sessionToken, Rect frame, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "relayoutMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId) + .relayoutMediaView(frame); + } catch (RemoteException | TvIAppManagerService.SessionNotFoundException e) { + Slog.e(TAG, "error in relayoutMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void removeMediaView(IBinder sessionToken, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "removeMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId) + .removeMediaView(); + } catch (RemoteException | TvIAppManagerService.SessionNotFoundException e) { + Slog.e(TAG, "error in removeMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } } @GuardedBy("mLock") |