Merge "Fix the IME flicker when the launching activity in fixed-rotation" into udc-dev

This commit is contained in:
Ming-Shin Lu
2023-04-29 02:06:14 +00:00
committed by Android (Google) Code Review
12 changed files with 216 additions and 52 deletions

View File

@@ -16,6 +16,7 @@
package android.window;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -23,6 +24,9 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.view.SurfaceControl;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Information when removing a starting window of a particular task.
* @hide
@@ -55,11 +59,28 @@ public final class StartingWindowRemovalInfo implements Parcelable {
*/
public boolean playRevealAnimation;
/** The mode is no need to defer removing the starting window for IME */
public static final int DEFER_MODE_NONE = 0;
/** The mode to defer removing the starting window until IME has drawn */
public static final int DEFER_MODE_NORMAL = 1;
/** The mode to defer the starting window removal until IME drawn and finished the rotation */
public static final int DEFER_MODE_ROTATION = 2;
@IntDef(prefix = { "DEFER_MODE_" }, value = {
DEFER_MODE_NONE,
DEFER_MODE_NORMAL,
DEFER_MODE_ROTATION,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DeferMode {}
/**
* Whether need to defer removing the starting window for IME.
* @hide
*/
public boolean deferRemoveForIme;
public @DeferMode int deferRemoveForImeMode;
/**
* The rounded corner radius
@@ -95,7 +116,7 @@ public final class StartingWindowRemovalInfo implements Parcelable {
windowAnimationLeash = source.readTypedObject(SurfaceControl.CREATOR);
mainFrame = source.readTypedObject(Rect.CREATOR);
playRevealAnimation = source.readBoolean();
deferRemoveForIme = source.readBoolean();
deferRemoveForImeMode = source.readInt();
roundedCornerRadius = source.readFloat();
windowlessSurface = source.readBoolean();
removeImmediately = source.readBoolean();
@@ -107,7 +128,7 @@ public final class StartingWindowRemovalInfo implements Parcelable {
dest.writeTypedObject(windowAnimationLeash, flags);
dest.writeTypedObject(mainFrame, flags);
dest.writeBoolean(playRevealAnimation);
dest.writeBoolean(deferRemoveForIme);
dest.writeInt(deferRemoveForImeMode);
dest.writeFloat(roundedCornerRadius);
dest.writeBoolean(windowlessSurface);
dest.writeBoolean(removeImmediately);
@@ -119,7 +140,7 @@ public final class StartingWindowRemovalInfo implements Parcelable {
+ " frame=" + mainFrame
+ " playRevealAnimation=" + playRevealAnimation
+ " roundedCornerRadius=" + roundedCornerRadius
+ " deferRemoveForIme=" + deferRemoveForIme
+ " deferRemoveForImeMode=" + deferRemoveForImeMode
+ " windowlessSurface=" + windowlessSurface
+ " removeImmediately=" + removeImmediately + "}";
}

View File

@@ -477,15 +477,15 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator {
}
@Override
public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
if (mRootView == null) {
return;
return true;
}
if (mSplashView == null) {
// shouldn't happen, the app window may be drawn earlier than starting window?
Slog.e(TAG, "Found empty splash screen, remove!");
removeWindowInner(mRootView, false);
return;
return true;
}
clearSystemBarColor();
if (immediately
@@ -503,6 +503,7 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator {
removeWindowInner(mRootView, true);
}
}
return true;
}
}
}

View File

@@ -18,6 +18,8 @@ package com.android.wm.shell.startingsurface;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION;
import android.annotation.CallSuper;
import android.app.TaskInfo;
@@ -216,7 +218,17 @@ public class StartingSurfaceDrawer {
}
abstract static class StartingWindowRecord {
protected int mBGColor;
abstract void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately);
/**
* Remove the starting window with the given {@link StartingWindowRemovalInfo} if possible.
* @param info The removal info sent from the task organizer controller in the WM core.
* @param immediately {@code true} means removing the starting window immediately,
* {@code false} otherwise.
* @return {@code true} means {@link StartingWindowRecordManager} can safely remove the
* record itself. {@code false} means {@link StartingWindowRecordManager} requires
* to manage the record reference and remove it later.
*/
abstract boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately);
int getBGColor() {
return mBGColor;
}
@@ -231,6 +243,15 @@ public class StartingSurfaceDrawer {
* {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}.
*/
private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600;
/**
* The max delay time in milliseconds for removing the task snapshot window with IME
* visible after the fixed rotation finished.
* Ideally the delay time will be shorter when receiving
* {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}.
*/
private static final long MAX_DELAY_REMOVAL_TIME_FIXED_ROTATION = 3000;
private final Runnable mScheduledRunnable = this::removeImmediately;
@WindowConfiguration.ActivityType protected final int mActivityType;
@@ -242,24 +263,34 @@ public class StartingSurfaceDrawer {
}
@Override
public final void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
public final boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
if (immediately) {
removeImmediately();
} else {
scheduleRemove(info.deferRemoveForIme);
scheduleRemove(info.deferRemoveForImeMode);
return false;
}
return true;
}
void scheduleRemove(boolean deferRemoveForIme) {
void scheduleRemove(@StartingWindowRemovalInfo.DeferMode int deferRemoveForImeMode) {
// Show the latest content as soon as possible for unlocking to home.
if (mActivityType == ACTIVITY_TYPE_HOME) {
removeImmediately();
return;
}
mRemoveExecutor.removeCallbacks(mScheduledRunnable);
final long delayRemovalTime = hasImeSurface() && deferRemoveForIme
? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE
: DELAY_REMOVAL_TIME_GENERAL;
final long delayRemovalTime;
switch (deferRemoveForImeMode) {
case DEFER_MODE_ROTATION:
delayRemovalTime = MAX_DELAY_REMOVAL_TIME_FIXED_ROTATION;
break;
case DEFER_MODE_NORMAL:
delayRemovalTime = MAX_DELAY_REMOVAL_TIME_IME_VISIBLE;
break;
default:
delayRemovalTime = DELAY_REMOVAL_TIME_GENERAL;
}
mRemoveExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"Defer removing snapshot surface in %d", delayRemovalTime);
@@ -297,8 +328,10 @@ public class StartingSurfaceDrawer {
final int taskId = removeInfo.taskId;
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
if (record != null) {
record.removeIfPossible(removeInfo, immediately);
mStartingWindowRecords.remove(taskId);
final boolean canRemoveRecord = record.removeIfPossible(removeInfo, immediately);
if (canRemoveRecord) {
mStartingWindowRecords.remove(taskId);
}
}
}

View File

@@ -124,7 +124,7 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator {
}
@Override
public void removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
if (!immediately) {
mSplashscreenContentDrawer.applyExitAnimation(mSplashView,
info.windowAnimationLeash, info.mainFrame,
@@ -132,6 +132,7 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator {
} else {
release();
}
return true;
}
void release() {

View File

@@ -265,17 +265,17 @@ public class StartingSurfaceDrawerTests extends ShellTestCase {
mStartingSurfaceDrawer.mWindowRecords.addRecord(taskId,
new StartingSurfaceDrawer.StartingWindowRecord() {
@Override
public void removeIfPossible(StartingWindowRemovalInfo info,
public boolean removeIfPossible(StartingWindowRemovalInfo info,
boolean immediately) {
return true;
}
});
mStartingSurfaceDrawer.mWindowlessRecords.addRecord(taskId,
new StartingSurfaceDrawer.StartingWindowRecord() {
@Override
public void removeIfPossible(StartingWindowRemovalInfo info,
public boolean removeIfPossible(StartingWindowRemovalInfo info,
boolean immediately) {
return true;
}
});
mStartingSurfaceDrawer.clearAllWindows();

View File

@@ -5220,6 +5220,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
logAppCompatState();
if (!visible) {
final InputTarget imeInputTarget = mDisplayContent.getImeInputTarget();
mLastImeShown = imeInputTarget != null && imeInputTarget.getWindowState() != null
&& imeInputTarget.getWindowState().mActivityRecord == this
&& mDisplayContent.mInputMethodWindow != null
&& mDisplayContent.mInputMethodWindow.isVisible();
finishOrAbortReplacingWindow();
}
return true;
@@ -5609,11 +5614,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
if (!visible) {
final InputTarget imeInputTarget = mDisplayContent.getImeInputTarget();
mLastImeShown = imeInputTarget != null && imeInputTarget.getWindowState() != null
&& imeInputTarget.getWindowState().mActivityRecord == this
&& mDisplayContent.mInputMethodWindow != null
&& mDisplayContent.mInputMethodWindow.isVisible();
mImeInsetsFrozenUntilStartInput = true;
}

View File

@@ -267,7 +267,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume
op.mDrawTransaction = null;
if (DEBUG) Slog.d(TAG, "finishOp merge transaction " + windowToken.getTopChild());
}
if (op.mAction == Operation.ACTION_FADE) {
if (op.mAction == Operation.ACTION_TOGGLE_IME) {
if (DEBUG) Slog.d(TAG, "finishOp fade-in IME " + windowToken.getTopChild());
fadeWindowToken(true /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM,
(type, anim) -> mDisplayContent.getInsetsStateController()
.getImeSourceProvider().reportImeDrawnForOrganizer());
} else if (op.mAction == Operation.ACTION_FADE) {
if (DEBUG) Slog.d(TAG, "finishOp fade-in " + windowToken.getTopChild());
// The previous animation leash will be dropped when preparing fade-in animation, so
// simply apply new animation without restoring the transformation.
@@ -344,7 +349,7 @@ class AsyncRotationController extends FadeAnimationController implements Consume
for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
final WindowToken windowToken = mTargetWindowTokens.keyAt(i);
final Operation op = mTargetWindowTokens.valueAt(i);
if (op.mAction == Operation.ACTION_FADE) {
if (op.mAction == Operation.ACTION_FADE || op.mAction == Operation.ACTION_TOGGLE_IME) {
fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
op.mLeash = windowToken.getAnimationLeash();
if (DEBUG) Slog.d(TAG, "Start fade-out " + windowToken.getTopChild());
@@ -374,17 +379,19 @@ class AsyncRotationController extends FadeAnimationController implements Consume
WindowManagerService.WINDOW_FREEZE_TIMEOUT_DURATION);
}
/** Hides the window immediately until it is drawn in new rotation. */
void hideImmediately(WindowToken windowToken) {
if (isTargetToken(windowToken)) return;
/** Hides the IME window immediately until it is drawn in new rotation. */
void hideImeImmediately() {
if (mDisplayContent.mInputMethodWindow == null) return;
final WindowToken imeWindowToken = mDisplayContent.mInputMethodWindow.mToken;
if (isTargetToken(imeWindowToken)) return;
final boolean original = mHideImmediately;
mHideImmediately = true;
final Operation op = new Operation(Operation.ACTION_FADE);
mTargetWindowTokens.put(windowToken, op);
fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
op.mLeash = windowToken.getAnimationLeash();
final Operation op = new Operation(Operation.ACTION_TOGGLE_IME);
mTargetWindowTokens.put(imeWindowToken, op);
fadeWindowToken(false /* show */, imeWindowToken, ANIMATION_TYPE_TOKEN_TRANSFORM);
op.mLeash = imeWindowToken.getAnimationLeash();
mHideImmediately = original;
if (DEBUG) Slog.d(TAG, "hideImmediately " + windowToken.getTopChild());
if (DEBUG) Slog.d(TAG, "hideImeImmediately " + imeWindowToken.getTopChild());
}
/** Returns {@code true} if the window will rotate independently. */
@@ -586,11 +593,13 @@ class AsyncRotationController extends FadeAnimationController implements Consume
/** The operation to control the rotation appearance associated with window token. */
private static class Operation {
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = { ACTION_SEAMLESS, ACTION_FADE })
@IntDef(value = { ACTION_SEAMLESS, ACTION_FADE, ACTION_TOGGLE_IME })
@interface Action {}
static final int ACTION_SEAMLESS = 1;
static final int ACTION_FADE = 2;
/** The action to toggle the IME window appearance */
static final int ACTION_TOGGLE_IME = 3;
final @Action int mAction;
/** The leash of window token. It can be animation leash or the token itself. */
SurfaceControl mLeash;

View File

@@ -1896,7 +1896,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
case SOFT_INPUT_STATE_HIDDEN:
return false;
}
return r.mLastImeShown;
final boolean useIme = r.getWindow(
w -> WindowManager.LayoutParams.mayUseInputMethod(w.mAttrs.flags)) != null;
if (!useIme) {
return false;
}
return r.mLastImeShown || (r.mStartingData != null && r.mStartingData.hasImeSurface());
}
/** Returns {@code true} if the top activity is transformed with the new rotation of display. */
@@ -4219,7 +4224,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
// Hide the window until the rotation is done to avoid intermediate artifacts if the
// parent surface of IME container is changed.
if (mAsyncRotationController != null) {
mAsyncRotationController.hideImmediately(mInputMethodWindow.mToken);
mAsyncRotationController.hideImeImmediately();
}
}
}

View File

@@ -57,14 +57,21 @@ public class FadeAnimationController {
return AnimationUtils.loadAnimation(mContext, R.anim.fade_out);
}
/** Run the fade in/out animation for a window token. */
public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType) {
fadeWindowToken(show, windowToken, animationType, null);
}
/**
* Run the fade in/out animation for a window token.
*
* @param show true for fade-in, otherwise for fade-out.
* @param windowToken the window token to run the animation.
* @param animationType the animation type defined in SurfaceAnimator.
* @param finishedCallback the callback after the animation finished.
*/
public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType) {
public void fadeWindowToken(boolean show, WindowToken windowToken, int animationType,
SurfaceAnimator.OnAnimationFinishedCallback finishedCallback) {
if (windowToken == null || windowToken.getParent() == null) {
return;
}
@@ -75,9 +82,8 @@ public class FadeAnimationController {
if (animationAdapter == null) {
return;
}
windowToken.startAnimation(windowToken.getPendingTransaction(), animationAdapter,
show /* hidden */, animationType, null /* finishedCallback */);
show /* hidden */, animationType, finishedCallback);
}
protected FadeAnimationAdapter createAdapter(LocalAnimationAdapter.AnimationSpec animationSpec,

View File

@@ -145,18 +145,44 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
}
boolean changed = super.updateClientVisibility(caller);
if (changed && caller.isRequestedVisible(mSource.getType())) {
reportImeDrawnForOrganizer(caller);
reportImeDrawnForOrganizerIfNeeded(caller);
}
changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate();
return changed;
}
private void reportImeDrawnForOrganizer(InsetsControlTarget caller) {
if (caller.getWindow() != null && caller.getWindow().getTask() != null) {
if (caller.getWindow().getTask().isOrganized()) {
mWindowContainer.mWmService.mAtmService.mTaskOrganizerController
.reportImeDrawnOnTask(caller.getWindow().getTask());
}
private void reportImeDrawnForOrganizerIfNeeded(@NonNull InsetsControlTarget caller) {
final WindowState callerWindow = caller.getWindow();
if (callerWindow == null) {
return;
}
WindowToken imeToken = mWindowContainer.asWindowState() != null
? mWindowContainer.asWindowState().mToken : null;
if (mDisplayContent.getAsyncRotationController() != null
&& mDisplayContent.getAsyncRotationController().isTargetToken(imeToken)) {
// Skip reporting IME drawn state when the control target is in fixed
// rotation, AsyncRotationController will report after the animation finished.
return;
}
reportImeDrawnForOrganizer(caller);
}
private void reportImeDrawnForOrganizer(@NonNull InsetsControlTarget caller) {
final WindowState callerWindow = caller.getWindow();
if (callerWindow == null || callerWindow.getTask() == null) {
return;
}
if (callerWindow.getTask().isOrganized()) {
mWindowContainer.mWmService.mAtmService.mTaskOrganizerController
.reportImeDrawnOnTask(caller.getWindow().getTask());
}
}
/** Report the IME has drawn on the current IME control target for its task organizer */
void reportImeDrawnForOrganizer() {
final InsetsControlTarget imeControlTarget = getControlTarget();
if (imeControlTarget != null) {
reportImeDrawnForOrganizer(imeControlTarget);
}
}

View File

@@ -18,6 +18,9 @@ package com.android.server.wm;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.TaskInfo.cameraCompatControlStateToString;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NONE;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER;
import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission;
@@ -686,8 +689,19 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub {
final boolean playShiftUpAnimation = !task.inMultiWindowMode();
final ActivityRecord topActivity = task.topActivityContainsStartingWindow();
if (topActivity != null) {
removalInfo.deferRemoveForIme = topActivity.mDisplayContent
.mayImeShowOnLaunchingActivity(topActivity);
// Set defer remove mode for IME
final DisplayContent dc = topActivity.getDisplayContent();
final WindowState imeWindow = dc.mInputMethodWindow;
if (topActivity.isVisibleRequested() && imeWindow != null
&& dc.mayImeShowOnLaunchingActivity(topActivity)
&& dc.isFixedRotationLaunchingApp(topActivity)) {
removalInfo.deferRemoveForImeMode = DEFER_MODE_ROTATION;
} else if (dc.mayImeShowOnLaunchingActivity(topActivity)) {
removalInfo.deferRemoveForImeMode = DEFER_MODE_NORMAL;
} else {
removalInfo.deferRemoveForImeMode = DEFER_MODE_NONE;
}
final WindowState mainWindow =
topActivity.findMainWindow(false/* includeStartingApp */);
// No app window for this activity, app might be crashed.

View File

@@ -18,6 +18,11 @@ package com.android.server.wm.flicker.ime
import android.platform.test.annotations.Presubmit
import android.tools.common.Rotation
import android.platform.test.annotations.Postsubmit
import android.tools.common.Timestamp
import android.tools.common.traces.component.ComponentNameMatcher
import android.tools.common.flicker.subject.exceptions.ExceptionMessageBuilder
import android.tools.common.flicker.subject.exceptions.InvalidPropertyException
import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.FlickerTest
@@ -36,7 +41,7 @@ import org.junit.runners.Parameterized
/**
* Test IME window layer will become visible when switching from the fixed orientation activity
* (e.g. Launcher activity). To run this test: `atest
* FlickerTests:OpenImeWindowFromFixedOrientationAppTest`
* FlickerTests:ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest`
*/
@RequiresDevice
@RunWith(Parameterized::class)
@@ -77,6 +82,49 @@ open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: Fl
flicker.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp)
}
@Postsubmit
@Test
fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() {
// Check if the snapshot appeared during the trace
var imeSnapshotRemovedTimestamp: Timestamp? = null
val layerTrace = flicker.reader.readLayersTrace()
val layerTraceEntries = layerTrace?.entries?.toList() ?: emptyList()
layerTraceEntries.zipWithNext { prev, next ->
val prevSnapshotLayerVisible =
ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(prev.visibleLayers)
val nextSnapshotLayerVisible =
ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(next.visibleLayers)
if (imeSnapshotRemovedTimestamp == null &&
(prevSnapshotLayerVisible && !nextSnapshotLayerVisible)) {
imeSnapshotRemovedTimestamp = next.timestamp
}
}
// if so, make an assertion
imeSnapshotRemovedTimestamp?.let { timestamp ->
val stateAfterSnapshot = layerTrace?.getEntryAt(timestamp)
?: error("State not found for $timestamp")
val imeLayers = ComponentNameMatcher.IME
.filterLayers(stateAfterSnapshot.visibleLayers.toList())
require(imeLayers.isNotEmpty()) { "IME layer not found" }
if (imeLayers.any { it.color.a != 1.0f }) {
val errorMsgBuilder = ExceptionMessageBuilder()
.setTimestamp(timestamp)
.forInvalidProperty("IME layer alpha")
.setExpected("is 1.0")
.setActual("not 1.0")
.addExtraDescription("Filter",
ComponentNameMatcher.IME.toLayerIdentifier())
throw InvalidPropertyException(errorMsgBuilder)
}
}
}
companion object {
/**
* Creates the test configurations.