diff options
7 files changed, 483 insertions, 2 deletions
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl index a747e4995869..c4806fba3585 100644 --- a/media/java/android/media/tv/ad/ITvAdManager.aidl +++ b/media/java/android/media/tv/ad/ITvAdManager.aidl @@ -16,6 +16,7 @@ package android.media.tv.ad; +import android.graphics.Rect; import android.media.tv.ad.ITvAdClient; import android.media.tv.ad.ITvAdManagerCallback; import android.media.tv.ad.TvAdServiceInfo; @@ -37,4 +38,9 @@ interface ITvAdManager { void registerCallback(in ITvAdManagerCallback callback, int userId); void unregisterCallback(in ITvAdManagerCallback callback, 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); } diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl index 751257ce4d4e..3ca0198ddf19 100644 --- a/media/java/android/media/tv/ad/ITvAdSession.aidl +++ b/media/java/android/media/tv/ad/ITvAdSession.aidl @@ -16,6 +16,7 @@ package android.media.tv.ad; +import android.graphics.Rect; import android.view.Surface; /** @@ -27,4 +28,8 @@ oneway interface ITvAdSession { void startAdService(); void setSurface(in Surface surface); void dispatchSurfaceChanged(int format, int width, int height); + + void createMediaView(in IBinder windowToken, in Rect frame); + void relayoutMediaView(in Rect frame); + void removeMediaView(); } diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java index 4df2783f6511..3d5bc89e2b17 100644 --- a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java +++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java @@ -17,6 +17,8 @@ package android.media.tv.ad; import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -43,6 +45,9 @@ public class ITvAdSessionWrapper private static final int DO_RELEASE = 1; private static final int DO_SET_SURFACE = 2; private static final int DO_DISPATCH_SURFACE_CHANGED = 3; + private static final int DO_CREATE_MEDIA_VIEW = 4; + private static final int DO_RELAYOUT_MEDIA_VIEW = 5; + private static final int DO_REMOVE_MEDIA_VIEW = 6; private final HandlerCaller mCaller; private TvAdService.Session mSessionImpl; @@ -61,6 +66,7 @@ public class ITvAdSessionWrapper @Override public void release() { + mSessionImpl.scheduleMediaViewCleanup(); mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); } @@ -97,6 +103,20 @@ public class ITvAdSessionWrapper args.recycle(); break; } + case DO_CREATE_MEDIA_VIEW: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.createMediaView((IBinder) args.arg1, (Rect) args.arg2); + args.recycle(); + break; + } + case DO_RELAYOUT_MEDIA_VIEW: { + mSessionImpl.relayoutMediaView((Rect) msg.obj); + break; + } + case DO_REMOVE_MEDIA_VIEW: { + mSessionImpl.removeMediaView(true); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -129,6 +149,22 @@ public class ITvAdSessionWrapper mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0)); } + @Override + public void createMediaView(IBinder windowToken, Rect frame) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_CREATE_MEDIA_VIEW, windowToken, frame)); + } + + @Override + public void relayoutMediaView(Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_MEDIA_VIEW, frame)); + } + + @Override + public void removeMediaView() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_MEDIA_VIEW)); + } + private final class TvAdEventReceiver extends InputEventReceiver { TvAdEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 9c7505197dec..b2ea00dd3593 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -21,6 +21,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.TvInputManager; import android.media.tv.flags.Flags; import android.os.Handler; @@ -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; @@ -286,6 +288,67 @@ public class TvAdManager { } /** + * 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 AD service. + * @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/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 699570397e34..6f7f67d365f7 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -20,19 +20,25 @@ import android.annotation.CallSuper; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Px; import android.annotation.SdkConstant; 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.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.util.Log; +import android.view.Gravity; import android.view.InputChannel; import android.view.InputDevice; import android.view.InputEvent; @@ -42,6 +48,7 @@ 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; @@ -56,6 +63,8 @@ public abstract class TvAdService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvAdService"; + private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000; + /** * Name under which a TvAdService component publishes information about itself. This meta-data * must reference an XML resource containing an @@ -151,7 +160,14 @@ public abstract class TvAdService extends Service { private final Context mContext; 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; /** @@ -166,6 +182,48 @@ public abstract class TvAdService extends Service { } /** + * 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 AD service can disable its media view when needed. + * + * @param enable {@code true} if you want to enable the media view. {@code false} + * otherwise. + * @hide + */ + @CallSuper + 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); + } + } + }); + } + + /** + * Returns {@code true} if media view is enabled, {@code false} otherwise. + * + * @see #setMediaViewEnabled(boolean) + * @hide + */ + public boolean isMediaViewEnabled() { + return mMediaViewEnabled; + } + + /** * Releases TvAdService session. */ public abstract void onRelease(); @@ -180,6 +238,9 @@ public abstract class TvAdService extends Service { 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); } /** @@ -307,6 +368,33 @@ public abstract class TvAdService 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 TvAdView}. 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, in pixels. + * @param height The height of the media view, in pixels. + * @hide + */ + public void onMediaViewSizeChanged(@Px int width, @Px int height) { + } + + /** + * Called when the application requests to create a media view. Each session + * implementation can override this method and return its own view. + * + * @return a view attached to the media window. {@code null} if no media view is created. + * @hide + */ + @Nullable + public View onCreateMediaView() { + return null; + } + + /** * Takes care of dispatching incoming input events and tells whether the event was handled. */ int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { @@ -388,6 +476,137 @@ public abstract class TvAdService extends Service { } } } + + /** + * Creates a 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 TvAdView'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; + } } diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index 5e67fe9f697b..5e4a70b03511 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -22,6 +22,8 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; @@ -64,6 +66,9 @@ public class TvAdView extends ViewGroup { private int mSurfaceViewTop; private int mSurfaceViewBottom; + private boolean mMediaViewCreated; + private Rect mMediaViewFrame; + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { @@ -121,6 +126,20 @@ public class TvAdView extends ViewGroup { mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE); } + /** @hide */ + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + createSessionMediaView(); + } + + /** @hide */ + @Override + public void onDetachedFromWindow() { + removeSessionMediaView(); + super.onDetachedFromWindow(); + } + @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { if (DEBUG) { @@ -150,6 +169,11 @@ public class TvAdView extends ViewGroup { public void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); mSurfaceView.setVisibility(visibility); + if (visibility == View.VISIBLE) { + createSessionMediaView(); + } else { + removeSessionMediaView(); + } } private void resetSurfaceView() { @@ -162,6 +186,7 @@ public class TvAdView extends ViewGroup { @Override protected void updateSurface() { super.updateSurface(); + relayoutSessionMediaView(); }}; // The surface view's content should be treated as secure all the time. mSurfaceView.setSecure(true); @@ -174,6 +199,69 @@ public class TvAdView extends ViewGroup { addView(mSurfaceView); } + /** + * Resets this TvAdView to release its resources. + * + * <p>It can be reused by call {@link #prepareAdService(String, String)}. + * @hide + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + resetInternal(); + } + + private void resetInternal() { + mSessionCallback = null; + if (mSession != null) { + setSessionSurface(null); + removeSessionMediaView(); + mUseRequestedSurfaceLayout = false; + mSession.release(); + mSession = null; + resetSurfaceView(); + } + } + + 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; @@ -185,7 +273,7 @@ public class TvAdView extends ViewGroup { if (mSession == null) { return; } - //mSession.dispatchSurfaceChanged(format, width, height); + mSession.dispatchSurfaceChanged(format, width, height); } /** @@ -246,6 +334,7 @@ public class TvAdView extends ViewGroup { dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); } } + createSessionMediaView(); } else { // Failed to create // Todo: forward error to Tv App @@ -262,6 +351,8 @@ public class TvAdView 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/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 9c60fbb6bb9a..f0c84370c922 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -1038,7 +1038,7 @@ public class TvInteractiveAppManagerService extends SystemService { } } finally { if (surface != null) { - // surface is not used in TvInteractiveAppManagerService. + // surface is not used in TvAdManagerService. surface.release(); } Binder.restoreCallingIdentity(identity); @@ -1106,6 +1106,67 @@ public class TvInteractiveAppManagerService extends SystemService { } } + @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 { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .createMediaView(windowToken, frame); + } catch (RemoteException | 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 { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .relayoutMediaView(frame); + } catch (RemoteException | 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 { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .removeMediaView(); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in removeMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } private final class BinderService extends ITvInteractiveAppManager.Stub { |