From a35b67997d1745d6d66ef647ae8cfe5456e10ccc Mon Sep 17 00:00:00 2001 From: Mariia Sandrikova Date: Fri, 23 Dec 2022 21:59:37 +0000 Subject: [PATCH] Enable forced focus for resumed split screen activities This is needed because some game engines wait until they get focus before drawing the content of the app so they are blacked out when they are resumed and do not have focus yet. Bug: 203398248 Test: atest FrameworksCoreTests:ViewRootImplTest#whenDispatchFakeFocus_noFocusAfterwards Change-Id: I5675b32fbb9f60a7373fadbce379d571168436e7 Merged-In: I5675b32fbb9f60a7373fadbce379d571168436e7 --- core/java/android/app/ActivityThread.java | 15 +++++++- .../android/app/ClientTransactionHandler.java | 3 +- .../servertransaction/ResumeActivityItem.java | 22 ++++++++--- .../TransactionExecutor.java | 3 +- .../TransactionExecutorHelper.java | 3 +- core/java/android/view/View.java | 16 ++++++++ core/java/android/view/ViewRootImpl.java | 38 ++++++++++++++++--- core/res/res/values/config.xml | 5 +++ core/res/res/values/symbols.xml | 1 + .../app/activity/ActivityThreadTest.java | 6 ++- .../servertransaction/ObjectPoolTests.java | 6 +-- .../TransactionParcelTests.java | 2 +- .../src/android/view/ViewRootImplTest.java | 14 +++++++ .../activity/ActivityThreadClientTest.java | 2 +- .../com/android/server/wm/ActivityRecord.java | 15 +++++++- .../server/wm/ActivityTaskSupervisor.java | 3 +- .../wm/DisplayRotationCompatPolicy.java | 3 +- .../server/wm/LetterboxConfiguration.java | 14 +++++++ .../com/android/server/wm/TaskFragment.java | 2 +- .../wm/DisplayRotationCompatPolicyTests.java | 3 +- 20 files changed, 149 insertions(+), 27 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 2beb64d98c99c..b26504dd1c496 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -4164,7 +4164,8 @@ public final class ActivityThread extends ClientTransactionHandler private void scheduleResume(ActivityClientRecord r) { final ClientTransaction transaction = ClientTransaction.obtain(this.mAppThread, r.token); - transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false)); + transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false, + /* shouldSendCompatFakeFocus */ false)); executeTransaction(transaction); } @@ -4791,7 +4792,7 @@ public final class ActivityThread extends ClientTransactionHandler @Override public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest, - boolean isForward, String reason) { + boolean isForward, boolean shouldSendCompatFakeFocus, String reason) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); @@ -4898,6 +4899,16 @@ public final class ActivityThread extends ClientTransactionHandler if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } + + if (shouldSendCompatFakeFocus) { + // Attaching to a window is asynchronous with the activity being resumed, + // so it's possible we will need to send a fake focus event after attaching + if (impl != null) { + impl.dispatchCompatFakeFocus(); + } else { + r.window.getDecorView().fakeFocusAfterAttachingToWindow(); + } + } } r.nextIdle = mNewActivities; diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 2c70c4e901576..d7fa732cfb3d4 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -109,7 +109,8 @@ public abstract class ClientTransactionHandler { * @param reason Reason for performing this operation. */ public abstract void handleResumeActivity(@NonNull ActivityClientRecord r, - boolean finalStateRequest, boolean isForward, String reason); + boolean finalStateRequest, boolean isForward, boolean shouldSendCompatFakeFocus, + String reason); /** * Notify the activity about top resumed state change. diff --git a/core/java/android/app/servertransaction/ResumeActivityItem.java b/core/java/android/app/servertransaction/ResumeActivityItem.java index e6fdc006615a1..222f8ca61e098 100644 --- a/core/java/android/app/servertransaction/ResumeActivityItem.java +++ b/core/java/android/app/servertransaction/ResumeActivityItem.java @@ -39,6 +39,9 @@ public class ResumeActivityItem extends ActivityLifecycleItem { private int mProcState; private boolean mUpdateProcState; private boolean mIsForward; + // Whether we should send compat fake focus when the activity is resumed. This is needed + // because some game engines wait to get focus before drawing the content of the app. + private boolean mShouldSendCompatFakeFocus; @Override public void preExecute(ClientTransactionHandler client, IBinder token) { @@ -52,7 +55,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); client.handleResumeActivity(r, true /* finalStateRequest */, mIsForward, - "RESUME_ACTIVITY"); + mShouldSendCompatFakeFocus, "RESUME_ACTIVITY"); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @@ -74,7 +77,8 @@ public class ResumeActivityItem extends ActivityLifecycleItem { private ResumeActivityItem() {} /** Obtain an instance initialized with provided params. */ - public static ResumeActivityItem obtain(int procState, boolean isForward) { + public static ResumeActivityItem obtain(int procState, boolean isForward, + boolean shouldSendCompatFakeFocus) { ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); if (instance == null) { instance = new ResumeActivityItem(); @@ -82,12 +86,13 @@ public class ResumeActivityItem extends ActivityLifecycleItem { instance.mProcState = procState; instance.mUpdateProcState = true; instance.mIsForward = isForward; + instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus; return instance; } /** Obtain an instance initialized with provided params. */ - public static ResumeActivityItem obtain(boolean isForward) { + public static ResumeActivityItem obtain(boolean isForward, boolean shouldSendCompatFakeFocus) { ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); if (instance == null) { instance = new ResumeActivityItem(); @@ -95,6 +100,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; instance.mUpdateProcState = false; instance.mIsForward = isForward; + instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus; return instance; } @@ -105,6 +111,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; mUpdateProcState = false; mIsForward = false; + mShouldSendCompatFakeFocus = false; ObjectPool.recycle(this); } @@ -117,6 +124,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { dest.writeInt(mProcState); dest.writeBoolean(mUpdateProcState); dest.writeBoolean(mIsForward); + dest.writeBoolean(mShouldSendCompatFakeFocus); } /** Read from Parcel. */ @@ -124,6 +132,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { mProcState = in.readInt(); mUpdateProcState = in.readBoolean(); mIsForward = in.readBoolean(); + mShouldSendCompatFakeFocus = in.readBoolean(); } public static final @NonNull Creator CREATOR = @@ -147,7 +156,8 @@ public class ResumeActivityItem extends ActivityLifecycleItem { } final ResumeActivityItem other = (ResumeActivityItem) o; return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState - && mIsForward == other.mIsForward; + && mIsForward == other.mIsForward + && mShouldSendCompatFakeFocus == other.mShouldSendCompatFakeFocus; } @Override @@ -156,12 +166,14 @@ public class ResumeActivityItem extends ActivityLifecycleItem { result = 31 * result + mProcState; result = 31 * result + (mUpdateProcState ? 1 : 0); result = 31 * result + (mIsForward ? 1 : 0); + result = 31 * result + (mShouldSendCompatFakeFocus ? 1 : 0); return result; } @Override public String toString() { return "ResumeActivityItem{procState=" + mProcState - + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + "}"; + + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + + ",shouldSendCompatFakeFocus=" + mShouldSendCompatFakeFocus + "}"; } } diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index 1ff0b796fb1ec..c8f7d100a3985 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -226,7 +226,8 @@ public class TransactionExecutor { break; case ON_RESUME: mTransactionHandler.handleResumeActivity(r, false /* finalStateRequest */, - r.isForward, "LIFECYCLER_RESUME_ACTIVITY"); + r.isForward, false /* shouldSendCompatFakeFocus */, + "LIFECYCLER_RESUME_ACTIVITY"); break; case ON_PAUSE: mTransactionHandler.handlePauseActivity(r, false /* finished */, diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java index 92f7dee0b0add..6c188795e028f 100644 --- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java +++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java @@ -195,7 +195,8 @@ public class TransactionExecutorHelper { lifecycleItem = StopActivityItem.obtain(0 /* configChanges */); break; default: - lifecycleItem = ResumeActivityItem.obtain(false /* isForward */); + lifecycleItem = ResumeActivityItem.obtain(false /* isForward */, + false /* shouldSendCompatFakeFocus */); break; } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index a78af04995d3d..333efad2c4c86 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -21264,6 +21264,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return Math.max(vis1, vis2); } + private boolean mShouldFakeFocus = false; + + /** + * Fake send a focus event after attaching to window. + * See {@link android.view.ViewRootImpl#dispatchCompatFakeFocus()} for details. + * @hide + */ + public void fakeFocusAfterAttachingToWindow() { + mShouldFakeFocus = true; + } + /** * @param info the {@link android.view.View.AttachInfo} to associated with * this view @@ -21332,6 +21343,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, notifyEnterOrExitForAutoFillIfNeeded(true); notifyAppearedOrDisappearedForContentCaptureIfNeeded(true); + + if (mShouldFakeFocus) { + getViewRootImpl().dispatchCompatFakeFocus(); + mShouldFakeFocus = false; + } } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index af070238787f5..6f4a63ca0fd72 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -258,6 +258,7 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV; private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV; private static final boolean DEBUG_BLAST = false || LOCAL_LOGV; + private static final int LOGTAG_INPUT_FOCUS = 62001; /** * Set to false if we do not want to use the multi threaded renderer even though @@ -3808,8 +3809,7 @@ public final class ViewRootImpl implements ViewParent, } if (mAdded) { - dispatchFocusEvent(hasWindowFocus); - + dispatchFocusEvent(hasWindowFocus, false /* fakeFocus */); // Note: must be done after the focus change callbacks, // so all of the view state is set up correctly. mImeFocusController.onPostWindowFocus( @@ -3844,7 +3844,33 @@ public final class ViewRootImpl implements ViewParent, } } - private void dispatchFocusEvent(boolean hasWindowFocus) { + /** + * Send a fake focus event for unfocused apps in split screen as some game engines wait to + * get focus before drawing the content of the app. This will be used so that apps do not get + * blacked out when they are resumed and do not have focus yet. + * + * {@hide} + */ + // TODO(b/263094829): Investigate dispatching this for onPause as well + public void dispatchCompatFakeFocus() { + boolean aboutToHaveFocus = false; + synchronized (this) { + aboutToHaveFocus = mWindowFocusChanged && mUpcomingWindowFocus; + } + final boolean alreadyHaveFocus = mAttachInfo.mHasWindowFocus; + if (aboutToHaveFocus || alreadyHaveFocus) { + // Do not need to toggle focus if app doesn't need it, or has focus. + return; + } + EventLog.writeEvent(LOGTAG_INPUT_FOCUS, + "Giving fake focus to " + mBasePackageName, "reason=unity bug workaround"); + dispatchFocusEvent(true /* hasWindowFocus */, true /* fakeFocus */); + EventLog.writeEvent(LOGTAG_INPUT_FOCUS, + "Removing fake focus from " + mBasePackageName, "reason=timeout callback"); + dispatchFocusEvent(false /* hasWindowFocus */, true /* fakeFocus */); + } + + private void dispatchFocusEvent(boolean hasWindowFocus, boolean fakeFocus) { profileRendering(hasWindowFocus); if (hasWindowFocus && mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) { mFullRedrawNeeded = true; @@ -3870,8 +3896,10 @@ public final class ViewRootImpl implements ViewParent, } mAttachInfo.mHasWindowFocus = hasWindowFocus; - mImeFocusController.updateImeFocusable(mWindowAttributes, true /* force */); - mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes); + if (!fakeFocus) { + mImeFocusController.updateImeFocusable(mWindowAttributes, true /* force */); + mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes); + } if (mView != null) { mAttachInfo.mKeyDispatchState.reset(); diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index a0d6e470041aa..a8c2246ef9660 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5344,6 +5344,11 @@ TODO(b/255532890) Enable when ignoreOrientationRequest is set --> false + + false +