TIAF: handle overlay media view

create/remove/relayout etc

Bug: 206826235
Test: mmm
Change-Id: Ia130985ed104ccf5473b50eba30394016700b66d
This commit is contained in:
shubang
2021-11-17 02:05:41 -08:00
parent d5a13dd765
commit 69e7905abf
6 changed files with 442 additions and 2 deletions

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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;
@@ -442,6 +444,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}.

View File

@@ -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,9 +170,40 @@ 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
@@ -194,6 +244,30 @@ public abstract class TvIAppService extends Service {
public void onNotifyBroadcastInfoResponse(BroadcastInfoResponse response) {
}
/**
* 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);

View File

@@ -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;
@@ -118,6 +123,18 @@ public class TvIAppView extends ViewGroup {
mTvIAppManager = (TvIAppManager) getContext().getSystemService("tv_interactive_app");
}
@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) {
@@ -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;
}

View File

@@ -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;
@@ -569,6 +570,11 @@ public class TvIAppManagerService extends SystemService {
return sessionState;
}
@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;
@@ -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")