diff options
author | Ming-Shin Lu <lumark@google.com> | 2021-08-10 01:39:40 +0800 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-09-20 22:29:57 +0000 |
commit | b831ff165c14d2cd25cfd4e7125b529569db2b97 (patch) | |
tree | 508c45440167e1e49fd98fd90cbfa844441a9a3a | |
parent | d7685815e4ee2a6992f0074377d16b2874238569 (diff) | |
download | base-b831ff165c14d2cd25cfd4e7125b529569db2b97.tar.gz |
Improve InsetsPolicy#adjustVisibilityForIme when switching apps
When switching tasks from the task has IME shown to another task
without showing IME (e.g. youtube apps.), the flickering might happen
because the next activity accidentally received the IME insets attached
in the previous activity.
If the IME is attached to one app window on that time, even the next
app window is behind the IME window, conceptually the window should
not receive the IME insets if the next window is not eligible IME
requester and ready to show IME on top of it.
Previously, we introduced CL[1] with checking task animating state and
check if the window has request IME visiblity to judge if the next
focus window is eligible to get IME insets.
However, CL[1] overlooked that some apps design may using a child
window to receive IME insets but not the IME requester (the requester
is activity main window), so makes the regression that the child window
may not able to receive IME to adjust editor layout on the main window
like Bug 195385541 and Bug 195846009.
As the result, in this CL we introduce another way to fix this improper
IME insets delivering.
The fix way is straight forward. when the task with showing IME is
leaving, we'll set a flag to froze IME insets state for all windows
until the next task's IME input target has focused and reprorted to
WM site.
As this fix approach could be more simple and preversing the original
insets behavior.
Refine the logic in InsetsPolicy#adjustVisibilityForIme with the above
approach.
[1]: Ib3a1eacedd3763bcf1e6eed82e7efdf01c50b204
Fix: 192337037
Test: atest WindowStateTests#\
testAdjustImeInsetsVisibilityWhenSwitchingApps
Test: atest ActivityRecordTests
Test: manual as below steps:
1) Switch tasks with the task has IME shown (e.g. Android Message)
and youtube apps.
2) Expect youtube no flickering when switched the task from
Android Message.
Change-Id: Id8a56c68a06a774ef12ba444fd9a368148ea4539
Merged-In: Id8a56c68a06a774ef12ba444fd9a368148ea4539
(cherry picked from commit 4950787a4313d8c0fa6685a9f3dbfb0b05671cf0)
6 files changed, 160 insertions, 10 deletions
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 308df2f4e9a0..c2cfe0b7bd92 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -656,6 +656,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean mLastImeShown; /** + * When set to true, the IME insets will be frozen until the next app becomes IME input target. + * @see InsetsPolicy#adjustVisibilityForIme + */ + boolean mImeInsetsFrozenUntilStartInput; + + /** * A flag to determine if this AR is in the process of closing or entering PIP. This is needed * to help AR know that the app is in the process of closing but hasn't yet started closing on * the WM side. @@ -1363,6 +1369,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (newTask != null && isState(RESUMED)) { newTask.setResumedActivity(this, "onParentChanged"); + mImeInsetsFrozenUntilStartInput = false; } if (rootTask != null && rootTask.topRunningActivity() == this) { @@ -4759,6 +4766,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A && imeInputTarget.getWindow().mActivityRecord == this && mDisplayContent.mInputMethodWindow != null && mDisplayContent.mInputMethodWindow.isVisible(); + mImeInsetsFrozenUntilStartInput = true; } final DisplayContent displayContent = getDisplayContent(); @@ -5877,6 +5885,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // closing activity having to wait until idle timeout to be stopped or destroyed if the // next activity won't report idle (e.g. repeated view animation). mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded(); + + // If the activity is visible, but no windows are eligible to start input, unfreeze + // to avoid permanently frozen IME insets. + if (mImeInsetsFrozenUntilStartInput && getWindow( + win -> WindowManager.LayoutParams.mayUseInputMethod(win.mAttrs.flags)) + == null) { + mImeInsetsFrozenUntilStartInput = false; + } } } @@ -7792,6 +7808,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + @Override + void onResize() { + // Reset freezing IME insets flag when the activity resized. + mImeInsetsFrozenUntilStartInput = false; + super.onResize(); + } + /** Returns true if the configuration is compatible with this activity. */ boolean isConfigurationCompatible(Configuration config) { final int orientation = getRequestedOrientation(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 6892dbc158d4..9335846e7805 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3971,6 +3971,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp void updateImeInputAndControlTarget(WindowState target) { if (mImeInputTarget != target) { ProtoLog.i(WM_DEBUG_IME, "setInputMethodInputTarget %s", target); + if (target != null && target.mActivityRecord != null) { + target.mActivityRecord.mImeInsetsFrozenUntilStartInput = false; + } setImeInputTarget(target); updateImeControlTarget(); } diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index f2f192686ad5..a8e1c1cda72b 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -211,7 +211,7 @@ class InsetsPolicy { InsetsState getInsetsForWindow(WindowState target) { final InsetsState originalState = mStateController.getInsetsForWindow(target); final InsetsState state = adjustVisibilityForTransientTypes(originalState); - return target.mIsImWindow ? adjustVisibilityForIme(state, state == originalState) : state; + return adjustVisibilityForIme(target, state, state == originalState); } /** @@ -241,16 +241,37 @@ class InsetsPolicy { return state; } - // Navigation bar insets is always visible to IME. - private static InsetsState adjustVisibilityForIme(InsetsState originalState, + private InsetsState adjustVisibilityForIme(WindowState w, InsetsState originalState, boolean copyState) { - final InsetsSource originalNavSource = originalState.peekSource(ITYPE_NAVIGATION_BAR); - if (originalNavSource != null && !originalNavSource.isVisible()) { - final InsetsState state = copyState ? new InsetsState(originalState) : originalState; - final InsetsSource navSource = new InsetsSource(originalNavSource); - navSource.setVisible(true); - state.addSource(navSource); - return state; + if (w.mIsImWindow) { + // Navigation bar insets is always visible to IME. + final InsetsSource originalNavSource = originalState.peekSource(ITYPE_NAVIGATION_BAR); + if (originalNavSource != null && !originalNavSource.isVisible()) { + final InsetsState state = copyState ? new InsetsState(originalState) + : originalState; + final InsetsSource navSource = new InsetsSource(originalNavSource); + navSource.setVisible(true); + state.addSource(navSource); + return state; + } + } else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) { + // During switching tasks with gestural navigation, if the IME is attached to + // one app window on that time, even the next app window is behind the IME window, + // conceptually the window should not receive the IME insets if the next window is + // not eligible IME requester and ready to show IME on top of it. + final boolean shouldImeAttachedToApp = mDisplayContent.shouldImeAttachedToApp(); + final InsetsSource originalImeSource = originalState.peekSource(ITYPE_IME); + + if (shouldImeAttachedToApp && originalImeSource != null) { + final boolean imeVisibility = + w.mActivityRecord.mLastImeShown || w.getRequestedVisibility(ITYPE_IME); + final InsetsState state = copyState ? new InsetsState(originalState) + : originalState; + final InsetsSource imeSource = new InsetsSource(originalImeSource); + imeSource.setVisible(imeVisibility); + state.addSource(imeSource); + return state; + } } return originalState; } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 293e862a6b74..6f04f176afd8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -39,6 +39,7 @@ import static android.os.Process.NOBODY_UID; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; +import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -2816,6 +2817,73 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); } + @Test + public void testImeInsetsFrozenFlag_resetWhenReparented() { + final ActivityRecord activity = createActivityWithTask(); + final WindowState app = createWindow(null, TYPE_APPLICATION, activity, "app"); + final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow"); + final Task newTask = new TaskBuilder(mSupervisor).build(); + makeWindowVisible(app, imeWindow); + mDisplayContent.mInputMethodWindow = imeWindow; + mDisplayContent.setImeLayeringTarget(app); + mDisplayContent.setImeInputTarget(app); + + // Simulate app is closing and expect the last IME is shown and IME insets is frozen. + app.mActivityRecord.commitVisibility(false, false); + assertTrue(app.mActivityRecord.mLastImeShown); + assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + + // Expect IME insets frozen state will reset when the activity is reparent to the new task. + activity.setState(RESUMED, "test"); + activity.reparent(newTask, 0 /* top */, "test"); + assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + } + + @UseTestDisplay(addWindows = W_INPUT_METHOD) + @Test + public void testImeInsetsFrozenFlag_resetWhenResized() { + final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + makeWindowVisibleAndDrawn(app, mImeWindow); + mDisplayContent.setImeLayeringTarget(app); + mDisplayContent.setImeInputTarget(app); + + // Simulate app is closing and expect the last IME is shown and IME insets is frozen. + app.mActivityRecord.commitVisibility(false, false); + assertTrue(app.mActivityRecord.mLastImeShown); + assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + + // Expect IME insets frozen state will reset when the activity is reparent to the new task. + app.mActivityRecord.onResize(); + assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + } + + @UseTestDisplay(addWindows = W_INPUT_METHOD) + @Test + public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { + final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + makeWindowVisibleAndDrawn(app, mImeWindow); + mDisplayContent.setImeLayeringTarget(app); + mDisplayContent.setImeInputTarget(app); + + // Simulate app is closing and expect the last IME is shown and IME insets is frozen. + app.mActivityRecord.commitVisibility(false, false); + app.mActivityRecord.onWindowsGone(); + + assertTrue(app.mActivityRecord.mLastImeShown); + assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + + // Expect IME insets frozen state will reset when the activity has no IME focusable window. + app.mActivityRecord.forAllWindowsUnchecked(w -> { + w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + return true; + }, true); + + app.mActivityRecord.commitVisibility(true, false); + app.mActivityRecord.onWindowsVisible(); + + assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + } + private void assertHasStartingWindow(ActivityRecord atoken) { assertNotNull(atoken.mStartingSurface); assertNotNull(atoken.mStartingData); diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index a8e17534e6ed..8e7ba4bc3293 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -258,6 +258,7 @@ public class SystemServicesTestRule implements TestRule { final ActivityManagerInternal amInternal = mAmService.mInternal; spyOn(amInternal); doNothing().when(amInternal).trimApplications(); + doNothing().when(amInternal).scheduleAppGcs(); doNothing().when(amInternal).updateCpuStats(); doNothing().when(amInternal).updateOomAdj(); doNothing().when(amInternal).updateBatteryStats(any(), anyInt(), anyInt(), anyBoolean()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 92b670ed9699..d88ac256be5c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -891,6 +891,40 @@ public class WindowStateTests extends WindowTestsBase { assertTrue(mAppWindow.getInsetsState().getSourceOrDefaultVisibility(ITYPE_NAVIGATION_BAR)); } + @Test + public void testAdjustImeInsetsVisibilityWhenSwitchingApps() { + final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2"); + final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow"); + spyOn(imeWindow); + doReturn(true).when(imeWindow).isVisible(); + mDisplayContent.mInputMethodWindow = imeWindow; + + final InsetsStateController controller = mDisplayContent.getInsetsStateController(); + controller.getImeSourceProvider().setWindow(imeWindow, null, null); + + // Simulate app requests IME with updating all windows Insets State when IME is above app. + mDisplayContent.setImeLayeringTarget(app); + mDisplayContent.setImeInputTarget(app); + assertTrue(mDisplayContent.shouldImeAttachedToApp()); + controller.getImeSourceProvider().scheduleShowImePostLayout(app); + controller.getImeSourceProvider().getSource().setVisible(true); + controller.updateAboveInsetsState(imeWindow, false); + + // Expect all app windows behind IME can receive IME insets visible. + assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible()); + assertTrue(app2.getInsetsState().getSource(ITYPE_IME).isVisible()); + + // Simulate app plays closing transition to app2. + app.mActivityRecord.commitVisibility(false, false); + assertTrue(app.mActivityRecord.mLastImeShown); + assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); + + // Verify the IME insets is visible on app, but not for app2 during app task switching. + assertTrue(app.getInsetsState().getSource(ITYPE_IME).isVisible()); + assertFalse(app2.getInsetsState().getSource(ITYPE_IME).isVisible()); + } + @UseTestDisplay(addWindows = { W_ACTIVITY }) @Test public void testUpdateImeControlTargetWhenLeavingMultiWindow() { |