diff options
8 files changed, 460 insertions, 4 deletions
diff --git a/media/java/android/media/tv/interactive/ITvIAppClient.aidl b/media/java/android/media/tv/interactive/ITvIAppClient.aidl index 419793413e39..0dd64b83df5f 100644 --- a/media/java/android/media/tv/interactive/ITvIAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppClient.aidl @@ -24,4 +24,5 @@ package android.media.tv.interactive; oneway interface ITvIAppClient { void onSessionCreated(in String iAppServiceId, IBinder token, int seq); void onSessionReleased(int seq); + void onLayoutSurface(int left, int top, int right, int bottom, int seq); }
\ No newline at end of file diff --git a/media/java/android/media/tv/interactive/ITvIAppManager.aidl b/media/java/android/media/tv/interactive/ITvIAppManager.aidl index a435e201030b..c7b08d50824d 100644 --- a/media/java/android/media/tv/interactive/ITvIAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppManager.aidl @@ -17,6 +17,7 @@ package android.media.tv.interactive; import android.media.tv.interactive.ITvIAppClient; +import android.view.Surface; /** * Interface to the TV interactive app service. @@ -27,4 +28,7 @@ interface ITvIAppManager { void createSession( in ITvIAppClient client, in String iAppServiceId, int type, int seq, int userId); void releaseSession(in IBinder sessionToken, int userId); + void setSurface(in IBinder sessionToken, in Surface surface, int userId); + void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height, + 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 0cbdc8eed6e5..0afa9716a783 100644 --- a/media/java/android/media/tv/interactive/ITvIAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppSession.aidl @@ -16,6 +16,8 @@ package android.media.tv.interactive; +import android.view.Surface; + /** * Sub-interface of ITvIAppService.aidl which is created per session and has its own context. * @hide @@ -23,4 +25,6 @@ package android.media.tv.interactive; oneway interface ITvIAppSession { void startIApp(); void release(); + void setSurface(in Surface surface); + void dispatchSurfaceChanged(int format, int width, int height); }
\ No newline at end of file diff --git a/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl index b3b317e808ac..0873aad8f5c6 100644 --- a/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvIAppSessionCallback.aidl @@ -25,4 +25,5 @@ import android.media.tv.interactive.ITvIAppSession; */ oneway interface ITvIAppSessionCallback { void onSessionCreated(in ITvIAppSession session); + void onLayoutSurface(int left, int top, int right, int bottom); }
\ 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 f6565340abd4..16e19e782761 100644 --- a/media/java/android/media/tv/interactive/TvIAppManager.java +++ b/media/java/android/media/tv/interactive/TvIAppManager.java @@ -25,6 +25,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; +import android.view.Surface; import com.android.internal.util.Preconditions; @@ -88,6 +89,18 @@ public final class TvIAppManager { record.postSessionReleased(); } } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postLayoutSurface(left, top, right, bottom); + } + } }; } @@ -159,6 +172,44 @@ public final class TvIAppManager { } /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render video. + */ + public void setSurface(Surface surface) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Notifies of any structural changes (format or size) of the surface passed in + * {@link #setSurface}. + * + * @param format The new PixelFormat of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void dispatchSurfaceChanged(int format, int width, int height) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Releases this session. */ public void release() { @@ -211,6 +262,16 @@ public final class TvIAppManager { } }); } + + void postLayoutSurface(final int left, final int top, final int right, + final int bottom) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom); + } + }); + } } /** @@ -235,5 +296,18 @@ public final class TvIAppManager { */ public void onSessionReleased(@NonNull Session session) { } + + /** + * This is called when {@link TvIAppService.Session#layoutSurface} is called to change the + * layout of surface. + * + * @param session A {@link TvIAppManager.Session} associated with this callback. + * @param left Left position. + * @param top Top position. + * @param right Right position. + * @param bottom Bottom position. + */ + public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { + } } } diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java index f363728181c6..b385d9c7c11a 100644 --- a/media/java/android/media/tv/interactive/TvIAppService.java +++ b/media/java/android/media/tv/interactive/TvIAppService.java @@ -16,10 +16,12 @@ package android.media.tv.interactive; +import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; @@ -27,6 +29,7 @@ import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.KeyEvent; +import android.view.Surface; import com.android.internal.os.SomeArgs; @@ -94,6 +97,20 @@ public abstract class TvIAppService extends Service { // @GuardedBy("mLock") private final List<Runnable> mPendingActions = new ArrayList<>(); + private final Context mContext; + private final Handler mHandler; + private Surface mSurface; + + /** + * Creates a new Session. + * + * @param context The context of the application + */ + public Session(Context context) { + mContext = context; + mHandler = new Handler(context.getMainLooper()); + } + /** * Starts TvIAppService session. */ @@ -101,16 +118,79 @@ public abstract class TvIAppService extends Service { } /** + * Called when the application sets the surface. + * + * <p>The TV IApp service should render interactive app UI onto the given surface. When + * called with {@code null}, the input service should immediately free any references to the + * currently set surface and stop using it. + * + * @param surface The surface to be used for interactive app UI rendering. Can be + * {@code null}. + * @return {@code true} if the surface was set successfully, {@code false} otherwise. + */ + public abstract boolean onSetSurface(@Nullable Surface surface); + + /** + * Called after any structural changes (format or size) have been made to the surface passed + * in {@link #onSetSurface}. This method is always called at least once, after + * {@link #onSetSurface} is called with non-null surface. + * + * @param format The new PixelFormat of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void onSurfaceChanged(int format, int width, int height) { + } + + /** * Releases TvIAppService session. */ public void onRelease() { } + /** + * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position + * is relative to the overlay view that sits on top of this surface. + * + * @param left Left position in pixels, relative to the overlay view. + * @param top Top position in pixels, relative to the overlay view. + * @param right Right position in pixels, relative to the overlay view. + * @param bottom Bottom position in pixels, relative to the overlay view. + */ + public void layoutSurface(final int left, final int top, final int right, + final int bottom) { + if (left > right || top > bottom) { + throw new IllegalArgumentException("Invalid parameter"); + } + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + + ", r=" + right + ", b=" + bottom + ",)"); + } + if (mSessionCallback != null) { + mSessionCallback.onLayoutSurface(left, top, right, bottom); + } + } catch (RemoteException e) { + Log.w(TAG, "error in layoutSurface", e); + } + } + }); + } + void startIApp() { onStartIApp(); } + void release() { onRelease(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } } private void initialize(ITvIAppSessionCallback callback) { @@ -122,6 +202,45 @@ public abstract class TvIAppService extends Service { mPendingActions.clear(); } } + + /** + * Calls {@link #onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + if (mSurface != null) { + mSurface.release(); + } + mSurface = surface; + // TODO: Handle failure. + } + + /** + * Calls {@link #onSurfaceChanged}. + */ + void dispatchSurfaceChanged(int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width + + ", height=" + height + ")"); + } + onSurfaceChanged(format, width, height); + } + + private void executeOrPostRunnableOnMainThread(Runnable action) { + synchronized (mLock) { + if (mSessionCallback == null) { + // The session is not initialized yet. + mPendingActions.add(action); + } else { + if (mHandler.getLooper().isCurrentThread()) { + action.run(); + } else { + // Posts the runnable if this is not called from the main thread + mHandler.post(action); + } + } + } + } } /** @@ -143,6 +262,16 @@ public abstract class TvIAppService extends Service { public void release() { mSessionImpl.release(); } + + @Override + public void setSurface(Surface surface) { + mSessionImpl.setSurface(surface); + } + + @Override + public void dispatchSurfaceChanged(int format, int width, int height) { + mSessionImpl.dispatchSurfaceChanged(format, width, height); + } } @SuppressLint("HandlerLeak") diff --git a/media/java/android/media/tv/interactive/TvIAppView.java b/media/java/android/media/tv/interactive/TvIAppView.java index f56ea0a87439..adaaab0905c5 100644 --- a/media/java/android/media/tv/interactive/TvIAppView.java +++ b/media/java/android/media/tv/interactive/TvIAppView.java @@ -17,10 +17,18 @@ package android.media.tv.interactive; import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; import android.media.tv.interactive.TvIAppManager.Session; import android.media.tv.interactive.TvIAppManager.SessionCallback; import android.os.Handler; +import android.util.AttributeSet; import android.util.Log; +import android.util.Xml; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; import android.view.ViewGroup; /** @@ -35,20 +43,139 @@ public class TvIAppView extends ViewGroup { private final Handler mHandler = new Handler(); private Session mSession; private MySessionCallback mSessionCallback; + private SurfaceView mSurfaceView; + private Surface mSurface; + + private boolean mSurfaceChanged; + private int mSurfaceFormat; + private int mSurfaceWidth; + private int mSurfaceHeight; + + private boolean mUseRequestedSurfaceLayout; + private int mSurfaceViewLeft; + private int mSurfaceViewRight; + private int mSurfaceViewTop; + private int mSurfaceViewBottom; + + private final AttributeSet mAttrs; + private final int mDefStyleAttr; + private final XmlResourceParser mParser; + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + + ", width=" + width + ", height=" + height + ")"); + } + mSurfaceFormat = format; + mSurfaceWidth = width; + mSurfaceHeight = height; + mSurfaceChanged = true; + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurface = null; + mSurfaceChanged = false; + setSessionSurface(null); + } + }; public TvIAppView(Context context) { + this(context, null, 0); + } + + public TvIAppView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, /* attrs = */null, /* defStyleAttr = */0); + int sourceResId = Resources.getAttributeSetSourceResId(attrs); + if (sourceResId != Resources.ID_NULL) { + Log.d(TAG, "Build local AttributeSet"); + mParser = context.getResources().getXml(sourceResId); + mAttrs = Xml.asAttributeSet(mParser); + } else { + Log.d(TAG, "Use passed in AttributeSet"); + mParser = null; + mAttrs = attrs; + } + mDefStyleAttr = defStyleAttr; + resetSurfaceView(); mTvIAppManager = (TvIAppManager) getContext().getSystemService("tv_interactive_app"); } @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (DEBUG) { - Log.d(TAG, - "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)"); + Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right + + ", bottom=" + bottom + ",)"); + } + if (mUseRequestedSurfaceLayout) { + mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight, + mSurfaceViewBottom); + } else { + mSurfaceView.layout(0, 0, right - left, bottom - top); } } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); + int width = mSurfaceView.getMeasuredWidth(); + int height = mSurfaceView.getMeasuredHeight(); + int childState = mSurfaceView.getMeasuredState(); + setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), + resolveSizeAndState(height, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + mSurfaceView.setVisibility(visibility); + } + + private void resetSurfaceView() { + if (mSurfaceView != null) { + mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); + removeView(mSurfaceView); + } + mSurface = null; + mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr); + // The surface view's content should be treated as secure all the time. + mSurfaceView.setSecure(true); + mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); + addView(mSurfaceView); + } + + /** + * Resets this TvIAppView. + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + resetInternal(); + } + + private void setSessionSurface(Surface surface) { + if (mSession == null) { + return; + } + mSession.setSurface(surface); + } + + private void dispatchSurfaceChanged(int format, int width, int height) { + if (mSession == null) { + return; + } + mSession.dispatchSurfaceChanged(format, width, height); + } + /** * Prepares the interactive application. */ @@ -75,6 +202,17 @@ public class TvIAppView extends ViewGroup { } } + private void resetInternal() { + mSessionCallback = null; + if (mSession != null) { + setSessionSurface(null); + mUseRequestedSurfaceLayout = false; + mSession.release(); + mSession = null; + resetSurfaceView(); + } + } + private class MySessionCallback extends SessionCallback { final String mIAppServiceId; int mType; @@ -99,7 +237,15 @@ public class TvIAppView extends ViewGroup { } mSession = session; if (session != null) { - // TODO: handle SurfaceView and InputChannel. + // mSurface may not be ready yet as soon as starting an application. + // In the case, we don't send Session.setSurface(null) unnecessarily. + // setSessionSurface will be called in surfaceCreated. + if (mSurface != null) { + setSessionSurface(mSurface); + if (mSurfaceChanged) { + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + } } else { // Failed to create // Todo: forward error to Tv App @@ -119,5 +265,23 @@ public class TvIAppView extends ViewGroup { mSessionCallback = null; mSession = null; } + + @Override + public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right=" + + right + ", bottom=" + bottom + ",)"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onLayoutSurface - session not created"); + return; + } + mSurfaceViewLeft = left; + mSurfaceViewTop = top; + mSurfaceViewRight = right; + mSurfaceViewBottom = bottom; + mUseRequestedSurfaceLayout = true; + requestLayout(); + } } } 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 d09ecebfcad1..cf212dffb373 100644 --- a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java @@ -36,6 +36,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.util.SparseArray; +import android.view.Surface; import com.android.internal.annotations.GuardedBy; import com.android.server.SystemService; @@ -136,6 +137,16 @@ public class TvIAppManagerService extends SystemService { return sessionState; } + @GuardedBy("mLock") + private ITvIAppSession getSessionLocked(SessionState sessionState) { + ITvIAppSession session = sessionState.mSession; + if (session == null) { + throw new IllegalStateException("Session not yet created for token " + + sessionState.mSessionToken); + } + return session; + } + private final class BinderService extends ITvIAppManager.Stub { @Override @@ -234,6 +245,55 @@ public class TvIAppManagerService extends SystemService { Slogf.e(TAG, "error in start", e); } } + + @Override + public void setSurface(IBinder sessionToken, Surface surface, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "setSurface"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).setSurface(surface); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in setSurface", e); + } + } + } finally { + if (surface != null) { + // surface is not used in TvIAppManagerService. + surface.release(); + } + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width, + int height, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "dispatchSurfaceChanged"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + SessionState sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).dispatchSurfaceChanged(format, width, + height); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in dispatchSurfaceChanged", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } } @GuardedBy("mLock") @@ -616,6 +676,25 @@ public class TvIAppManagerService extends SystemService { } } + @Override + public void onLayoutSurface(int left, int top, int right, int bottom) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + + ", right=" + right + ", bottom=" + bottom + ",)"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onLayoutSurface(left, top, right, bottom, + mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onLayoutSurface", e); + } + } + } + @GuardedBy("mLock") private boolean addSessionTokenToClientStateLocked(ITvIAppSession session) { try { |