summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java26
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java255
-rw-r--r--libs/WindowManager/Jetpack/window-extensions-release.aarbin21364 -> 21513 bytes
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java43
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java234
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java290
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java212
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java255
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java79
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java83
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java111
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java30
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java76
43 files changed, 2013 insertions, 307 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index bdf703c9bd38..7e9c4189dabb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -20,11 +20,14 @@ import android.app.ActivityThread;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.area.WindowAreaComponentImpl;
import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
import androidx.window.extensions.embedding.SplitController;
import androidx.window.extensions.layout.WindowLayoutComponent;
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
+
/**
* The reference implementation of {@link WindowExtensions} that implements the initial API version.
*/
@@ -33,10 +36,12 @@ public class WindowExtensionsImpl implements WindowExtensions {
private final Object mLock = new Object();
private volatile WindowLayoutComponent mWindowLayoutComponent;
private volatile SplitController mSplitController;
+ private volatile WindowAreaComponent mWindowAreaComponent;
+ // TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return 1;
+ return 2;
}
/**
@@ -75,4 +80,23 @@ public class WindowExtensionsImpl implements WindowExtensions {
}
return mSplitController;
}
+
+ /**
+ * Returns a reference implementation of {@link WindowAreaComponent} if available,
+ * {@code null} otherwise. The implementation must match the API level reported in
+ * {@link WindowExtensions#getWindowAreaComponent()}.
+ * @return {@link WindowAreaComponent} OEM implementation.
+ */
+ public WindowAreaComponent getWindowAreaComponent() {
+ if (mWindowAreaComponent == null) {
+ synchronized (mLock) {
+ if (mWindowAreaComponent == null) {
+ Context context = ActivityThread.currentApplication();
+ mWindowAreaComponent =
+ new WindowAreaComponentImpl(context);
+ }
+ }
+ }
+ return mWindowAreaComponent;
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
new file mode 100644
index 000000000000..3adae7006369
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.extensions.area;
+
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Reference implementation of androidx.window.extensions.area OEM interface for use with
+ * WindowManager Jetpack.
+ *
+ * This component currently supports Rear Display mode with the ability to add and remove
+ * status listeners for this mode.
+ *
+ * The public methods in this class are thread-safe.
+ **/
+public class WindowAreaComponentImpl implements WindowAreaComponent,
+ DeviceStateManager.DeviceStateCallback {
+
+ private final Object mLock = new Object();
+
+ private final DeviceStateManager mDeviceStateManager;
+ private final Executor mExecutor;
+
+ @GuardedBy("mLock")
+ private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
+ private final int mRearDisplayState;
+ @WindowAreaSessionState
+ private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
+
+ @GuardedBy("mLock")
+ private int mCurrentDeviceState = INVALID_DEVICE_STATE;
+ @GuardedBy("mLock")
+ private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE;
+ @GuardedBy("mLock")
+ private DeviceStateRequest mDeviceStateRequest;
+
+ public WindowAreaComponentImpl(@NonNull Context context) {
+ mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+ mExecutor = context.getMainExecutor();
+
+ // TODO(b/236022708) Move rear display state to device state config file
+ mRearDisplayState = context.getResources().getInteger(
+ R.integer.config_deviceStateRearDisplay);
+
+ mDeviceStateManager.registerCallback(mExecutor, this);
+ }
+
+ /**
+ * Adds a listener interested in receiving updates on the RearDisplayStatus
+ * of the device. Because this is being called from the OEM provided
+ * extensions, we will post the result of the listener on the executor
+ * provided by the developer at the initial call site.
+ *
+ * Depending on the initial state of the device, we will return either
+ * {@link WindowAreaComponent#STATUS_AVAILABLE} or
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
+ * state respectively. When the rear display feature is triggered, we update the status to be
+ * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_
+ *
+ * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently
+ * enabled.
+ *
+ * @param consumer {@link Consumer} interested in receiving updates to the status of
+ * rear display mode.
+ */
+ public void addRearDisplayStatusListener(
+ @NonNull Consumer<@WindowAreaStatus Integer> consumer) {
+ synchronized (mLock) {
+ mRearDisplayStatusListeners.add(consumer);
+
+ // If current device state is still invalid, we haven't gotten our initial value yet
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ return;
+ }
+ consumer.accept(getCurrentStatus());
+ }
+ }
+
+ /**
+ * Removes a listener no longer interested in receiving updates.
+ * @param consumer no longer interested in receiving updates to RearDisplayStatus
+ */
+ public void removeRearDisplayStatusListener(
+ @NonNull Consumer<@WindowAreaStatus Integer> consumer) {
+ synchronized (mLock) {
+ mRearDisplayStatusListeners.remove(consumer);
+ }
+ }
+
+ /**
+ * Creates and starts a rear display session and provides updates to the
+ * callback provided. Because this is being called from the OEM provided
+ * extensions, we will post the result of the listener on the executor
+ * provided by the developer at the initial call site.
+ *
+ * When we enable rear display mode, we submit a request to {@link DeviceStateManager}
+ * to override the device state to the state that corresponds to RearDisplay
+ * mode. When the {@link DeviceStateRequest} is activated, we let the
+ * consumer know that the session is active by sending
+ * {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
+ *
+ * @param activity to provide updates to the client on
+ * the status of the Session
+ * @param rearDisplaySessionCallback to provide updates to the client on
+ * the status of the Session
+ */
+ public void startRearDisplaySession(@NonNull Activity activity,
+ @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
+ synchronized (mLock) {
+ if (mDeviceStateRequest != null) {
+ // Rear display session is already active
+ throw new IllegalStateException(
+ "Unable to start new rear display session as one is already active");
+ }
+ mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
+ mDeviceStateManager.requestState(
+ mDeviceStateRequest,
+ mExecutor,
+ new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback)
+ );
+ }
+ }
+
+ /**
+ * Ends the current rear display session and provides updates to the
+ * callback provided. Because this is being called from the OEM provided
+ * extensions, we will post the result of the listener on the executor
+ * provided by the developer.
+ */
+ public void endRearDisplaySession() {
+ synchronized (mLock) {
+ if (mDeviceStateRequest != null || isRearDisplayActive()) {
+ mDeviceStateRequest = null;
+ mDeviceStateManager.cancelStateRequest();
+ } else {
+ throw new IllegalStateException(
+ "Unable to cancel a rear display session as there is no active session");
+ }
+ }
+ }
+
+ @Override
+ public void onBaseStateChanged(int state) {
+ synchronized (mLock) {
+ mCurrentDeviceBaseState = state;
+ if (state == mCurrentDeviceState) {
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+ }
+
+ @Override
+ public void onStateChanged(int state) {
+ synchronized (mLock) {
+ mCurrentDeviceState = state;
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+
+ @GuardedBy("mLock")
+ private int getCurrentStatus() {
+ if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
+ || isRearDisplayActive()) {
+ return WindowAreaComponent.STATUS_UNAVAILABLE;
+ }
+ return WindowAreaComponent.STATUS_AVAILABLE;
+ }
+
+ /**
+ * Helper method to determine if a rear display session is currently active by checking
+ * if the current device configuration matches that of rear display. This would be true
+ * if there is a device override currently active (base state != current state) and the current
+ * state is that which corresponds to {@code mRearDisplayState}
+ * @return {@code true} if the device is in rear display mode and {@code false} if not
+ */
+ @GuardedBy("mLock")
+ private boolean isRearDisplayActive() {
+ return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState
+ == mRearDisplayState);
+ }
+
+ @GuardedBy("mLock")
+ private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) {
+ synchronized (mLock) {
+ for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
+ mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
+ }
+ }
+ }
+
+ /**
+ * Callback for the {@link DeviceStateRequest} to be notified of when the request has been
+ * activated or cancelled. This callback provides information to the client library
+ * on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
+ */
+ private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
+
+ private final Consumer<Integer> mRearDisplaySessionCallback;
+
+ DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
+ mRearDisplaySessionCallback = callback;
+ }
+
+ @Override
+ public void onRequestActivated(@NonNull DeviceStateRequest request) {
+ synchronized (mLock) {
+ if (request.equals(mDeviceStateRequest)) {
+ mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
+ mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+ }
+
+ @Override
+ public void onRequestCanceled(DeviceStateRequest request) {
+ synchronized (mLock) {
+ if (request.equals(mDeviceStateRequest)) {
+ mDeviceStateRequest = null;
+ }
+ mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
+ mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
+ updateStatusConsumers(getCurrentStatus());
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 918e514f4c89..e9a1721fba2a 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index d28a68a42b2b..a8764e05c3e2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -54,7 +54,10 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
/** Callback for listening task state. */
public interface Listener {
- /** Called when the container is ready for launching activities. */
+ /**
+ * Only called once when the surface has been created & the container is ready for
+ * launching activities.
+ */
default void onInitialized() {}
/** Called when the container can no longer launch activities. */
@@ -80,12 +83,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
private final SyncTransactionQueue mSyncQueue;
private final TaskViewTransitions mTaskViewTransitions;
- private ActivityManager.RunningTaskInfo mTaskInfo;
+ protected ActivityManager.RunningTaskInfo mTaskInfo;
private WindowContainerToken mTaskToken;
private SurfaceControl mTaskLeash;
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mSurfaceCreated;
private boolean mIsInitialized;
+ private boolean mNotifiedForInitialized;
private Listener mListener;
private Executor mListenerExecutor;
private Region mObscuredTouchRegion;
@@ -110,6 +114,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
mGuard.open("release");
}
+ /**
+ * @return {@code True} when the TaskView's surface has been created, {@code False} otherwise.
+ */
+ public boolean isInitialized() {
+ return mIsInitialized;
+ }
+
/** Until all users are converted, we may have mixed-use (eg. Car). */
private boolean isUsingShellTransitions() {
return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS;
@@ -269,11 +280,17 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
resetTaskInfo();
});
mGuard.close();
- if (mListener != null && mIsInitialized) {
+ mIsInitialized = false;
+ notifyReleased();
+ }
+
+ /** Called when the {@link TaskView} has been released. */
+ protected void notifyReleased() {
+ if (mListener != null && mNotifiedForInitialized) {
mListenerExecutor.execute(() -> {
mListener.onReleased();
});
- mIsInitialized = false;
+ mNotifiedForInitialized = false;
}
}
@@ -407,12 +424,8 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceCreated = true;
- if (mListener != null && !mIsInitialized) {
- mIsInitialized = true;
- mListenerExecutor.execute(() -> {
- mListener.onInitialized();
- });
- }
+ mIsInitialized = true;
+ notifyInitialized();
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
// Nothing to update, task is not yet available
@@ -430,6 +443,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
});
}
+ /** Called when the {@link TaskView} is initialized. */
+ protected void notifyInitialized() {
+ if (mListener != null && !mNotifiedForInitialized) {
+ mNotifiedForInitialized = true;
+ mListenerExecutor.execute(() -> {
+ mListener.onInitialized();
+ });
+ }
+ }
+
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mTaskToken == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
new file mode 100644
index 000000000000..cc4db933ec9f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.activityembedding;
+
+import static android.graphics.Matrix.MSCALE_X;
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
+
+import android.annotation.CallSuper;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Wrapper to handle the ActivityEmbedding animation update in one
+ * {@link SurfaceControl.Transaction}.
+ */
+class ActivityEmbeddingAnimationAdapter {
+
+ /**
+ * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer.
+ */
+ private static final int LAYER_NO_OVERRIDE = -1;
+
+ final Animation mAnimation;
+ final TransitionInfo.Change mChange;
+ final SurfaceControl mLeash;
+
+ final Transformation mTransformation = new Transformation();
+ final float[] mMatrix = new float[9];
+ final float[] mVecs = new float[4];
+ final Rect mRect = new Rect();
+ private boolean mIsFirstFrame = true;
+ private int mOverrideLayer = LAYER_NO_OVERRIDE;
+
+ ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+ @NonNull TransitionInfo.Change change) {
+ this(animation, change, change.getLeash());
+ }
+
+ /**
+ * @param leash the surface to animate, which is not necessary the same as
+ * {@link TransitionInfo.Change#getLeash()}, it can be a screenshot for example.
+ */
+ ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
+ @NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash) {
+ mAnimation = animation;
+ mChange = change;
+ mLeash = leash;
+ }
+
+ /**
+ * Surface layer to be set at the first frame of the animation. We will not set the layer if it
+ * is set to {@link #LAYER_NO_OVERRIDE}.
+ */
+ final void overrideLayer(int layer) {
+ mOverrideLayer = layer;
+ }
+
+ /** Called on frame update. */
+ final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+ if (mIsFirstFrame) {
+ t.show(mLeash);
+ if (mOverrideLayer != LAYER_NO_OVERRIDE) {
+ t.setLayer(mLeash, mOverrideLayer);
+ }
+ mIsFirstFrame = false;
+ }
+
+ // Extract the transformation to the current time.
+ mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
+ mTransformation);
+ t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ onAnimationUpdateInner(t);
+ }
+
+ /** To be overridden by subclasses to adjust the animation surface change. */
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ // Get current animation position.
+ final int positionX = Math.round(mMatrix[MTRANS_X]);
+ final int positionY = Math.round(mMatrix[MTRANS_Y]);
+ // The exiting surface starts at position: Change#getEndRelOffset() and moves with
+ // positionX varying. Offset our crop region by the amount we have slided so crop
+ // regions stays exactly on the original container in split.
+ final int cropOffsetX = offset.x - positionX;
+ final int cropOffsetY = offset.y - positionY;
+ final Rect cropRect = new Rect();
+ cropRect.set(mChange.getEndAbsBounds());
+ // Because window crop uses absolute position.
+ cropRect.offsetTo(0, 0);
+ cropRect.offset(cropOffsetX, cropOffsetY);
+ t.setCrop(mLeash, cropRect);
+ }
+
+ /** Called after animation finished. */
+ @CallSuper
+ void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+ onAnimationUpdate(t, mAnimation.getDuration());
+ }
+
+ final long getDurationHint() {
+ return mAnimation.computeDurationHint();
+ }
+
+ /**
+ * Should be used when the {@link TransitionInfo.Change} is in split with others, and wants to
+ * animate together as one. This adapter will offset the animation leash to make the animate of
+ * two windows look like a single window.
+ */
+ static class SplitAdapter extends ActivityEmbeddingAnimationAdapter {
+ private final boolean mIsLeftHalf;
+ private final int mWholeAnimationWidth;
+
+ /**
+ * @param isLeftHalf whether this is the left half of the animation.
+ * @param wholeAnimationWidth the whole animation windows width.
+ */
+ SplitAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+ boolean isLeftHalf, int wholeAnimationWidth) {
+ super(animation, change);
+ mIsLeftHalf = isLeftHalf;
+ mWholeAnimationWidth = wholeAnimationWidth;
+ if (wholeAnimationWidth == 0) {
+ throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
+ }
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ float posX = offset.x;
+ final float posY = offset.y;
+ // This window is half of the whole animation window. Offset left/right to make it
+ // look as one with the other half.
+ mTransformation.getMatrix().getValues(mMatrix);
+ final int changeWidth = mChange.getEndAbsBounds().width();
+ final float scaleX = mMatrix[MSCALE_X];
+ final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
+ final float curOffset = changeWidth * (1 - scaleX) / 2;
+ final float offsetDiff = totalOffset - curOffset;
+ if (mIsLeftHalf) {
+ posX += offsetDiff;
+ } else {
+ posX -= offsetDiff;
+ }
+ mTransformation.getMatrix().postTranslate(posX, posY);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ }
+ }
+
+ /**
+ * Should be used for the animation of the snapshot of a {@link TransitionInfo.Change} that has
+ * size change.
+ */
+ static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter {
+
+ SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl snapshotLeash) {
+ super(animation, change, snapshotLeash);
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ // Snapshot should always be placed at the top left of the animation leash.
+ mTransformation.getMatrix().postTranslate(0, 0);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+ }
+
+ @Override
+ void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+ super.onAnimationEnd(t);
+ // Remove the screenshot leash after animation is finished.
+ t.remove(mLeash);
+ }
+ }
+
+ /**
+ * Should be used for the animation of the {@link TransitionInfo.Change} that has size change.
+ */
+ static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter {
+
+ BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) {
+ super(animation, change);
+ }
+
+ @Override
+ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+ final Point offset = mChange.getEndRelOffset();
+ mTransformation.getMatrix().postTranslate(offset.x, offset.y);
+ t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+ t.setAlpha(mLeash, mTransformation.getAlpha());
+
+ // The following applies an inverse scale to the clip-rect so that it crops "after" the
+ // scale instead of before.
+ mVecs[1] = mVecs[2] = 0;
+ mVecs[0] = mVecs[3] = 1;
+ mTransformation.getMatrix().mapVectors(mVecs);
+ mVecs[0] = 1.f / mVecs[0];
+ mVecs[3] = 1.f / mVecs[3];
+ final Rect clipRect = mTransformation.getClipRect();
+ mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
+ mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
+ mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
+ mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
+ t.setCrop(mLeash, mRect);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
new file mode 100644
index 000000000000..7e0795d11153
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.common.ScreenshotUtils;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+/** To run the ActivityEmbedding animations. */
+class ActivityEmbeddingAnimationRunner {
+
+ private static final String TAG = "ActivityEmbeddingAnimR";
+
+ private final ActivityEmbeddingController mController;
+ @VisibleForTesting
+ final ActivityEmbeddingAnimationSpec mAnimationSpec;
+
+ ActivityEmbeddingAnimationRunner(@NonNull Context context,
+ @NonNull ActivityEmbeddingController controller) {
+ mController = controller;
+ mAnimationSpec = new ActivityEmbeddingAnimationSpec(context);
+ }
+
+ /** Creates and starts animation for ActivityEmbedding transition. */
+ void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final Animator animator = createAnimator(info, startTransaction, finishTransaction,
+ () -> mController.onAnimationFinished(transition));
+ startTransaction.apply();
+ animator.start();
+ }
+
+ /**
+ * Sets transition animation scale settings value.
+ * @param scale The setting value of transition animation scale.
+ */
+ void setAnimScaleSetting(float scale) {
+ mAnimationSpec.setAnimScaleSetting(scale);
+ }
+
+ /** Creates the animator for the given {@link TransitionInfo}. */
+ @VisibleForTesting
+ @NonNull
+ Animator createAnimator(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Runnable animationFinishCallback) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters =
+ createAnimationAdapters(info, startTransaction);
+ long duration = 0;
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ duration = Math.max(duration, adapter.getDurationHint());
+ }
+ final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.setDuration(duration);
+ animator.addUpdateListener((anim) -> {
+ // Update all adapters in the same transaction.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
+ }
+ t.apply();
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
+ adapter.onAnimationEnd(t);
+ }
+ t.apply();
+ animationFinishCallback.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+ return animator;
+ }
+
+ /**
+ * Creates list of {@link ActivityEmbeddingAnimationAdapter} to handle animations on all window
+ * changes.
+ */
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getMode() == TRANSIT_CHANGE
+ && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ return createChangeAnimationAdapters(info, startTransaction);
+ }
+ }
+ if (Transitions.isClosingType(info.getType())) {
+ return createCloseAnimationAdapters(info);
+ }
+ return createOpenAnimationAdapters(info);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, true /* isOpening */,
+ mAnimationSpec::loadOpenAnimation);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
+ @NonNull TransitionInfo info) {
+ return createOpenCloseAnimationAdapters(info, false /* isOpening */,
+ mAnimationSpec::loadCloseAnimation);
+ }
+
+ /**
+ * Creates {@link ActivityEmbeddingAnimationAdapter} for OPEN and CLOSE types of transition.
+ * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
+ */
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
+ @NonNull TransitionInfo info, boolean isOpening,
+ @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider) {
+ // We need to know if the change window is only a partial of the whole animation screen.
+ // If so, we will need to adjust it to make the whole animation screen looks like one.
+ final List<TransitionInfo.Change> openingChanges = new ArrayList<>();
+ final List<TransitionInfo.Change> closingChanges = new ArrayList<>();
+ final Rect openingWholeScreenBounds = new Rect();
+ final Rect closingWholeScreenBounds = new Rect();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ final Rect bounds = new Rect(change.getEndAbsBounds());
+ final Point offset = change.getEndRelOffset();
+ bounds.offsetTo(offset.x, offset.y);
+ if (Transitions.isOpeningType(change.getMode())) {
+ openingChanges.add(change);
+ openingWholeScreenBounds.union(bounds);
+ } else {
+ closingChanges.add(change);
+ closingWholeScreenBounds.union(bounds);
+ }
+ }
+
+ // For OPEN transition, open windows should be above close windows.
+ // For CLOSE transition, open windows should be below close windows.
+ int offsetLayer = TYPE_LAYER_OFFSET;
+ final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+ for (TransitionInfo.Change change : openingChanges) {
+ final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+ change, animationProvider, openingWholeScreenBounds);
+ if (isOpening) {
+ adapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(adapter);
+ }
+ for (TransitionInfo.Change change : closingChanges) {
+ final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
+ change, animationProvider, closingWholeScreenBounds);
+ if (!isOpening) {
+ adapter.overrideLayer(offsetLayer++);
+ }
+ adapters.add(adapter);
+ }
+ return adapters;
+ }
+
+ @NonNull
+ private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
+ @NonNull TransitionInfo.Change change,
+ @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider,
+ @NonNull Rect wholeAnimationBounds) {
+ final Animation animation = animationProvider.apply(change, wholeAnimationBounds);
+ final Rect bounds = new Rect(change.getEndAbsBounds());
+ final Point offset = change.getEndRelOffset();
+ bounds.offsetTo(offset.x, offset.y);
+ if (bounds.left == wholeAnimationBounds.left
+ && bounds.right != wholeAnimationBounds.right) {
+ // This is the left split of the whole animation window.
+ return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
+ true /* isLeftHalf */, wholeAnimationBounds.width());
+ } else if (bounds.left != wholeAnimationBounds.left
+ && bounds.right == wholeAnimationBounds.right) {
+ // This is the right split of the whole animation window.
+ return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
+ false /* isLeftHalf */, wholeAnimationBounds.width());
+ }
+ // Open/close window that fills the whole animation.
+ return new ActivityEmbeddingAnimationAdapter(animation, change);
+ }
+
+ @NonNull
+ private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters(
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (change.getMode() == TRANSIT_CHANGE
+ && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
+ // This is the window with bounds change.
+ final WindowContainerToken parentToken = change.getParent();
+ final Rect parentBounds;
+ if (parentToken != null) {
+ TransitionInfo.Change parentChange = info.getChange(parentToken);
+ parentBounds = parentChange != null
+ ? parentChange.getEndAbsBounds()
+ : change.getEndAbsBounds();
+ } else {
+ parentBounds = change.getEndAbsBounds();
+ }
+ final Animation[] animations =
+ mAnimationSpec.createChangeBoundsChangeAnimations(change, parentBounds);
+ // Adapter for the starting screenshot leash.
+ final SurfaceControl screenshotLeash = createScreenshot(change, startTransaction);
+ if (screenshotLeash != null) {
+ // The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd
+ adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter(
+ animations[0], change, screenshotLeash));
+ } else {
+ Log.e(TAG, "Failed to take screenshot for change=" + change);
+ }
+ // Adapter for the ending bounds changed leash.
+ adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter(
+ animations[1], change));
+ continue;
+ }
+
+ // These are the other windows that don't have bounds change in the same transition.
+ final Animation animation;
+ if (!TransitionInfo.isIndependent(change, info)) {
+ // No-op if it will be covered by the changing parent window.
+ animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
+ } else if (Transitions.isClosingType(change.getMode())) {
+ animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
+ } else {
+ animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
+ }
+ adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
+ }
+ return adapters;
+ }
+
+ /** Takes a screenshot of the given {@link TransitionInfo.Change} surface. */
+ @Nullable
+ private SurfaceControl createScreenshot(@NonNull TransitionInfo.Change change,
+ @NonNull SurfaceControl.Transaction startTransaction) {
+ final Rect cropBounds = new Rect(change.getStartAbsBounds());
+ cropBounds.offsetTo(0, 0);
+ return ScreenshotUtils.takeScreenshot(startTransaction, change.getLeash(), cropBounds,
+ Integer.MAX_VALUE);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
new file mode 100644
index 000000000000..6f06f28caff2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.activityembedding;
+
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.ClipRectAnimation;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
+import android.window.TransitionInfo;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
+import com.android.wm.shell.transition.Transitions;
+
+/** Animation spec for ActivityEmbedding transition. */
+// TODO(b/206557124): provide an easier way to customize animation
+class ActivityEmbeddingAnimationSpec {
+
+ private static final String TAG = "ActivityEmbeddingAnimSpec";
+ private static final int CHANGE_ANIMATION_DURATION = 517;
+ private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
+ private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
+
+ private final Context mContext;
+ private final TransitionAnimation mTransitionAnimation;
+ private final Interpolator mFastOutExtraSlowInInterpolator;
+ private final LinearInterpolator mLinearInterpolator;
+ private float mTransitionAnimationScaleSetting;
+
+ ActivityEmbeddingAnimationSpec(@NonNull Context context) {
+ mContext = context;
+ mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
+ mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
+ mContext, android.R.interpolator.fast_out_extra_slow_in);
+ mLinearInterpolator = new LinearInterpolator();
+ }
+
+ /**
+ * Sets transition animation scale settings value.
+ * @param scale The setting value of transition animation scale.
+ */
+ void setAnimScaleSetting(float scale) {
+ mTransitionAnimationScaleSetting = scale;
+ }
+
+ /** For window that doesn't need to be animated. */
+ @NonNull
+ static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
+ // Noop but just keep the window showing/hiding.
+ final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f;
+ return new AlphaAnimation(alpha, alpha);
+ }
+
+ /** Animation for window that is opening in a change transition. */
+ @NonNull
+ Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change) {
+ final Rect bounds = change.getEndAbsBounds();
+ final Point offset = change.getEndRelOffset();
+ // The window will be animated in from left or right depends on its position.
+ final int startLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /** Animation for window that is closing in a change transition. */
+ @NonNull
+ Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change) {
+ final Rect bounds = change.getEndAbsBounds();
+ final Point offset = change.getEndRelOffset();
+ // The window will be animated out to left or right depends on its position.
+ final int endLeft = offset.x == 0 ? -bounds.width() : bounds.width();
+
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
+ animation.setInterpolator(mFastOutExtraSlowInInterpolator);
+ animation.setDuration(CHANGE_ANIMATION_DURATION);
+ animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ /**
+ * Animation for window that is changing (bounds change) in a change transition.
+ * @return the return array always has two elements. The first one is for the start leash, and
+ * the second one is for the end leash.
+ */
+ @NonNull
+ Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
+ @NonNull Rect parentBounds) {
+ // Both start bounds and end bounds are in screen coordinates. We will post translate
+ // to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Rect startBounds = change.getStartAbsBounds();
+ final Rect endBounds = change.getEndAbsBounds();
+ float scaleX = ((float) startBounds.width()) / endBounds.width();
+ float scaleY = ((float) startBounds.height()) / endBounds.height();
+ // Start leash is a child of the end leash. Reverse the scale so that the start leash won't
+ // be scaled up with its parent.
+ float startScaleX = 1.f / scaleX;
+ float startScaleY = 1.f / scaleY;
+
+ // The start leash will be fade out.
+ final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
+ final Animation startAlpha = new AlphaAnimation(1f, 0f);
+ startAlpha.setInterpolator(mLinearInterpolator);
+ startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
+ startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
+ startSet.addAnimation(startAlpha);
+ final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
+ startScaleY);
+ startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
+ startScale.setDuration(CHANGE_ANIMATION_DURATION);
+ startSet.addAnimation(startScale);
+ startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
+ endBounds.height());
+ startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ // The end leash will be moved into the end position while scaling.
+ final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
+ endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
+ final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
+ endScale.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endScale);
+ // The position should be 0-based as we will post translate in
+ // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
+ final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
+ 0, 0);
+ endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(endTranslate);
+ // The end leash is resizing, we should update the window crop based on the clip rect.
+ final Rect startClip = new Rect(startBounds);
+ final Rect endClip = new Rect(endBounds);
+ startClip.offsetTo(0, 0);
+ endClip.offsetTo(0, 0);
+ final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
+ clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
+ endSet.addAnimation(clipAnim);
+ endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
+ parentBounds.height());
+ endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+
+ return new Animation[]{startSet, endSet};
+ }
+
+ @NonNull
+ Animation loadOpenAnimation(@NonNull TransitionInfo.Change change,
+ @NonNull Rect wholeAnimationBounds) {
+ final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final Animation animation;
+ // TODO(b/207070762):
+ // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
+ // 2. Implement edgeExtension version
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? R.anim.task_fragment_open_enter
+ : R.anim.task_fragment_open_exit);
+ final Rect bounds = change.getEndAbsBounds();
+ animation.initialize(bounds.width(), bounds.height(),
+ wholeAnimationBounds.width(), wholeAnimationBounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+
+ @NonNull
+ Animation loadCloseAnimation(@NonNull TransitionInfo.Change change,
+ @NonNull Rect wholeAnimationBounds) {
+ final boolean isEnter = Transitions.isOpeningType(change.getMode());
+ final Animation animation;
+ // TODO(b/207070762):
+ // 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
+ // 2. Implement edgeExtension version
+ animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
+ ? R.anim.task_fragment_close_enter
+ : R.anim.task_fragment_close_exit);
+ final Rect bounds = change.getEndAbsBounds();
+ animation.initialize(bounds.width(), bounds.height(),
+ wholeAnimationBounds.width(), wholeAnimationBounds.height());
+ animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+ return animation;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index b305897b77ae..e0004fcaa060 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -18,8 +18,11 @@ package com.android.wm.shell.activityembedding;
import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+import static java.util.Objects.requireNonNull;
+
import android.content.Context;
import android.os.IBinder;
+import android.util.ArrayMap;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
@@ -28,6 +31,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -37,15 +41,37 @@ import com.android.wm.shell.transition.Transitions;
public class ActivityEmbeddingController implements Transitions.TransitionHandler {
private final Context mContext;
- private final Transitions mTransitions;
-
- public ActivityEmbeddingController(Context context, ShellInit shellInit,
- Transitions transitions) {
- mContext = context;
- mTransitions = transitions;
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- shellInit.addInitCallback(this::onInit, this);
- }
+ @VisibleForTesting
+ final Transitions mTransitions;
+ @VisibleForTesting
+ final ActivityEmbeddingAnimationRunner mAnimationRunner;
+
+ /**
+ * Keeps track of the currently-running transition callback associated with each transition
+ * token.
+ */
+ private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks =
+ new ArrayMap<>();
+
+ private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit,
+ @NonNull Transitions transitions) {
+ mContext = requireNonNull(context);
+ mTransitions = requireNonNull(transitions);
+ mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this);
+
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ /**
+ * Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not
+ * supported.
+ */
+ @Nullable
+ public static ActivityEmbeddingController create(@NonNull Context context,
+ @NonNull ShellInit shellInit, @NonNull Transitions transitions) {
+ return Transitions.ENABLE_SHELL_TRANSITIONS
+ ? new ActivityEmbeddingController(context, shellInit, transitions)
+ : null;
}
/** Registers to handle transitions. */
@@ -66,9 +92,9 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
}
}
- // TODO(b/207070762) Implement AE animation.
- startTransaction.apply();
- finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ // Start ActivityEmbedding animation.
+ mTransitionCallbacks.put(transition, finishCallback);
+ mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction);
return true;
}
@@ -79,6 +105,21 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
return null;
}
+ @Override
+ public void setAnimScaleSetting(float scale) {
+ mAnimationRunner.setAnimScaleSetting(scale);
+ }
+
+ /** Called when the animation is finished. */
+ void onAnimationFinished(@NonNull IBinder transition) {
+ final Transitions.TransitionFinishCallback callback =
+ mTransitionCallbacks.remove(transition);
+ if (callback == null) {
+ throw new IllegalStateException("No finish callback found");
+ }
+ callback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ }
+
private static boolean isEmbedded(@NonNull TransitionInfo.Change change) {
return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 2c02006c8ca5..99b8885acdef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -821,7 +821,7 @@ public class Bubble implements BubbleViewProvider {
/**
* Description of current bubble state.
*/
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ public void dump(@NonNull PrintWriter pw) {
pw.print("key: "); pw.println(mKey);
pw.print(" showInShade: "); pw.println(showInShade());
pw.print(" showDot: "); pw.println(showDot());
@@ -831,7 +831,7 @@ public class Bubble implements BubbleViewProvider {
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
if (mExpandedView != null) {
- mExpandedView.dump(pw, args);
+ mExpandedView.dump(pw);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index de26b54971ca..dcbb272feab8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -72,7 +72,6 @@ import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.util.Log;
import android.util.Pair;
-import android.util.Slog;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
@@ -100,6 +99,7 @@ import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -159,6 +159,7 @@ public class BubbleController implements ConfigurationChangeListener {
private final TaskViewTransitions mTaskViewTransitions;
private final SyncTransactionQueue mSyncQueue;
private final ShellController mShellController;
+ private final ShellCommandHandler mShellCommandHandler;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -229,6 +230,7 @@ public class BubbleController implements ConfigurationChangeListener {
public BubbleController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
BubbleData data,
@Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
@@ -252,6 +254,7 @@ public class BubbleController implements ConfigurationChangeListener {
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
mLauncherApps = launcherApps;
mBarService = statusBarService == null
@@ -431,6 +434,7 @@ public class BubbleController implements ConfigurationChangeListener {
mCurrentProfiles = userProfiles;
mShellController.addConfigurationChangeListener(this);
+ mShellCommandHandler.addDumpCallback(this::dump, this);
}
@VisibleForTesting
@@ -538,7 +542,6 @@ public class BubbleController implements ConfigurationChangeListener {
if (mNotifEntryToExpandOnShadeUnlock != null) {
expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
- mNotifEntryToExpandOnShadeUnlock = null;
}
updateStack();
@@ -925,15 +928,6 @@ public class BubbleController implements ConfigurationChangeListener {
return (isSummary && isSuppressedSummary) || isSuppressedBubble;
}
- private void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback) {
- if (mBubbleData.isSummarySuppressed(groupKey)) {
- mBubbleData.removeSuppressedSummary(groupKey);
- if (callback != null) {
- callback.accept(mBubbleData.getSummaryKey(groupKey));
- }
- }
- }
-
/** Promote the provided bubble from the overflow view. */
public void promoteBubbleFromOverflow(Bubble bubble) {
mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK);
@@ -1519,14 +1513,15 @@ public class BubbleController implements ConfigurationChangeListener {
/**
* Description of current bubble state.
*/
- private void dump(PrintWriter pw, String[] args) {
+ private void dump(PrintWriter pw, String prefix) {
pw.println("BubbleController state:");
- mBubbleData.dump(pw, args);
+ mBubbleData.dump(pw);
pw.println();
if (mStackView != null) {
- mStackView.dump(pw, args);
+ mStackView.dump(pw);
}
pw.println();
+ mImpl.mCachedState.dump(pw);
}
/**
@@ -1711,28 +1706,12 @@ public class BubbleController implements ConfigurationChangeListener {
}
@Override
- public boolean isStackExpanded() {
- return mCachedState.isStackExpanded();
- }
-
- @Override
@Nullable
public Bubble getBubbleWithShortcutId(String shortcutId) {
return mCachedState.getBubbleWithShortcutId(shortcutId);
}
@Override
- public void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
- Executor callbackExecutor) {
- mMainExecutor.execute(() -> {
- Consumer<String> cb = callback != null
- ? (key) -> callbackExecutor.execute(() -> callback.accept(key))
- : null;
- BubbleController.this.removeSuppressedSummaryIfNecessary(groupKey, cb);
- });
- }
-
- @Override
public void collapseStack() {
mMainExecutor.execute(() -> {
BubbleController.this.collapseStack();
@@ -1761,13 +1740,6 @@ public class BubbleController implements ConfigurationChangeListener {
}
@Override
- public void openBubbleOverflow() {
- mMainExecutor.execute(() -> {
- BubbleController.this.openBubbleOverflow();
- });
- }
-
- @Override
public boolean handleDismissalInterception(BubbleEntry entry,
@Nullable List<BubbleEntry> children, IntConsumer removeCallback,
Executor callbackExecutor) {
@@ -1882,18 +1854,6 @@ public class BubbleController implements ConfigurationChangeListener {
mMainExecutor.execute(
() -> BubbleController.this.onNotificationPanelExpandedChanged(expanded));
}
-
- @Override
- public void dump(PrintWriter pw, String[] args) {
- try {
- mMainExecutor.executeBlocking(() -> {
- BubbleController.this.dump(pw, args);
- mCachedState.dump(pw);
- });
- } catch (InterruptedException e) {
- Slog.e(TAG, "Failed to dump BubbleController in 2s");
- }
- }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index fa86c8436647..c64133f0b668 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -1136,7 +1136,7 @@ public class BubbleData {
/**
* Description of current bubble data state.
*/
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.print("selected: ");
pw.println(mSelectedBubble != null
? mSelectedBubble.getKey()
@@ -1147,13 +1147,13 @@ public class BubbleData {
pw.print("stack bubble count: ");
pw.println(mBubbles.size());
for (Bubble bubble : mBubbles) {
- bubble.dump(pw, args);
+ bubble.dump(pw);
}
pw.print("overflow bubble count: ");
pw.println(mOverflowBubbles.size());
for (Bubble bubble : mOverflowBubbles) {
- bubble.dump(pw, args);
+ bubble.dump(pw);
}
pw.print("summaryKeys: ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 4f225fff1451..840b2856270c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -1044,7 +1044,7 @@ public class BubbleExpandedView extends LinearLayout {
/**
* Description of current expanded view state.
*/
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ public void dump(@NonNull PrintWriter pw) {
pw.print("BubbleExpandedView");
pw.print(" taskId: "); pw.println(mTaskId);
pw.print(" stackView: "); pw.println(mStackView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 2d0be066beb5..5bf88b119661 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -299,7 +299,7 @@ public class BubbleStackView extends FrameLayout
private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker;
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("Stack view state:");
String bubblesOnScreen = BubbleDebugConfig.formatBubblesString(
@@ -313,8 +313,8 @@ public class BubbleStackView extends FrameLayout
pw.print(" expandedContainerMatrix: ");
pw.println(mExpandedViewContainer.getAnimationMatrix());
- mStackAnimationController.dump(pw, args);
- mExpandedAnimationController.dump(pw, args);
+ mStackAnimationController.dump(pw);
+ mExpandedAnimationController.dump(pw);
if (mExpandedBubble != null) {
pw.println("Expanded bubble state:");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 37b96ffe5cd1..0e97e9e74114 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -35,7 +35,6 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.common.annotations.ExternalThread;
-import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.HashMap;
@@ -91,18 +90,6 @@ public interface Bubbles {
*/
boolean isBubbleExpanded(String key);
- /** @return {@code true} if stack of bubbles is expanded or not. */
- boolean isStackExpanded();
-
- /**
- * Removes a group key indicating that the summary for this group should no longer be
- * suppressed.
- *
- * @param callback If removed, this callback will be called with the summary key of the group
- */
- void removeSuppressedSummaryIfNecessary(String groupKey, Consumer<String> callback,
- Executor callbackExecutor);
-
/** Tell the stack of bubbles to collapse. */
void collapseStack();
@@ -130,9 +117,6 @@ public interface Bubbles {
/** Called for any taskbar changes. */
void onTaskbarChanged(Bundle b);
- /** Open the overflow view. */
- void openBubbleOverflow();
-
/**
* We intercept notification entries (including group summaries) dismissed by the user when
* there is an active bubble associated with it. We do this so that developers can still
@@ -252,9 +236,6 @@ public interface Bubbles {
*/
void onUserRemoved(int removedUserId);
- /** Description of current bubble state. */
- void dump(PrintWriter pw, String[] args);
-
/** Listener to find out about stack expansion / collapse events. */
interface BubbleExpandListener {
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index b521cb6a3d38..ae434bcec6c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -468,7 +468,7 @@ public class ExpandedAnimationController
}
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("ExpandedAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" animatingExpand: "); pw.println(mAnimatingExpand);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index 0a1b4d70fb2b..4e2cbfd82fcc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -431,7 +431,7 @@ public class StackAnimationController extends
}
/** Description of current animation controller state. */
- public void dump(PrintWriter pw, String[] args) {
+ public void dump(PrintWriter pw) {
pw.println("StackAnimationController state:");
pw.print(" isActive: "); pw.println(isActiveController());
pw.print(" restingStackPos: ");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 85c8ebf454c9..83ba909e712d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -80,7 +80,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public static final int PARALLAX_DISMISSING = 1;
public static final int PARALLAX_ALIGN_CENTER = 2;
- private static final int FLING_ANIMATION_DURATION = 250;
+ private static final int FLING_RESIZE_DURATION = 250;
+ private static final int FLING_SWITCH_DURATION = 350;
+ private static final int FLING_ENTER_DURATION = 350;
+ private static final int FLING_EXIT_DURATION = 350;
private final int mDividerWindowWidth;
private final int mDividerInsets;
@@ -93,6 +96,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
private final Rect mBounds1 = new Rect();
// Bounds2 final position should be always at bottom or right
private final Rect mBounds2 = new Rect();
+ // The temp bounds outside of display bounds for side stage when split screen inactive to avoid
+ // flicker next time active split screen.
+ private final Rect mInvisibleBounds = new Rect();
private final Rect mWinBounds1 = new Rect();
private final Rect mWinBounds2 = new Rect();
private final SplitLayoutHandler mSplitLayoutHandler;
@@ -141,6 +147,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
resetDividerPosition();
mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide);
+
+ mInvisibleBounds.set(mRootBounds);
+ mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0,
+ isLandscape() ? 0 : mRootBounds.bottom);
}
private int getDividerInsets(Resources resources, Display display) {
@@ -239,6 +249,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
rect.offset(-mRootBounds.left, -mRootBounds.top);
}
+ /** Gets bounds size equal to root bounds but outside of screen, used for position side stage
+ * when split inactive to avoid flicker when next time active. */
+ public void getInvisibleBounds(Rect rect) {
+ rect.set(mInvisibleBounds);
+ }
+
/** Returns leash of the current divider bar. */
@Nullable
public SurfaceControl getDividerLeash() {
@@ -284,6 +300,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
initDividerPosition(mTempRect);
+ mInvisibleBounds.set(mRootBounds);
+ mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0,
+ isLandscape() ? 0 : mRootBounds.bottom);
+
return true;
}
@@ -405,6 +425,13 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
mFreezeDividerWindow = freezeDividerWindow;
}
+ /** Update current layout as divider put on start or end position. */
+ public void setDividerAtBorder(boolean start) {
+ final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
+ : mDividerSnapAlgorithm.getDismissEndTarget().position;
+ setDividePosition(pos, false /* applyLayoutChange */);
+ }
+
/**
* Updates bounds with the passing position. Usually used to update recording bounds while
* performing animation or dragging divider bar to resize the splits.
@@ -449,17 +476,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
switch (snapTarget.flag) {
case FLAG_DISMISS_START:
- flingDividePosition(currentPosition, snapTarget.position,
+ flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
case FLAG_DISMISS_END:
- flingDividePosition(currentPosition, snapTarget.position,
+ flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
default:
- flingDividePosition(currentPosition, snapTarget.position,
+ flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
break;
}
@@ -516,12 +543,20 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void flingDividerToDismiss(boolean toEnd, int reason) {
final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
: mDividerSnapAlgorithm.getDismissStartTarget().position;
- flingDividePosition(getDividePosition(), target,
+ flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
}
+ /** Fling divider from current position to center position. */
+ public void flingDividerToCenter() {
+ final int pos = mDividerSnapAlgorithm.getMiddleTarget().position;
+ flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION,
+ () -> setDividePosition(pos, true /* applyLayoutChange */));
+ }
+
@VisibleForTesting
- void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
+ void flingDividePosition(int from, int to, int duration,
+ @Nullable Runnable flingFinishedCallback) {
if (from == to) {
// No animation run, still callback to stop resizing.
mSplitLayoutHandler.onLayoutSizeChanged(this);
@@ -531,7 +566,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
ValueAnimator animator = ValueAnimator
.ofInt(from, to)
- .setDuration(FLING_ANIMATION_DURATION);
+ .setDuration(duration);
animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
animator.addUpdateListener(
animation -> updateDivideBounds((int) animation.getAnimatedValue()));
@@ -588,7 +623,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2, animator3);
- set.setDuration(FLING_ANIMATION_DURATION);
+ set.setDuration(FLING_SWITCH_DURATION);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index e22c9517f4ab..8022e9b1cd81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -66,6 +66,7 @@ public abstract class TvPipModule {
@Provides
static Optional<Pip> providePip(
Context context,
+ ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -84,6 +85,7 @@ public abstract class TvPipModule {
return Optional.of(
TvPipController.create(
context,
+ shellInit,
shellController,
tvPipBoundsState,
tvPipBoundsAlgorithm,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index a6a04cf67b3c..7a736ccab5d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -85,6 +85,7 @@ import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.UnfoldAnimationController;
import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.util.Optional;
@@ -294,25 +295,33 @@ public abstract class WMShellBaseModule {
// Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@BindsOptionalOf
@DynamicOverride
- abstract FullscreenTaskListener optionalFullscreenTaskListener();
+ abstract FullscreenTaskListener<?> optionalFullscreenTaskListener();
@WMSingleton
@Provides
- static FullscreenTaskListener provideFullscreenTaskListener(
- @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener,
+ static FullscreenTaskListener<?> provideFullscreenTaskListener(
+ @DynamicOverride Optional<FullscreenTaskListener<?>> fullscreenTaskListener,
ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
SyncTransactionQueue syncQueue,
- Optional<RecentTasksController> recentTasksOptional) {
+ Optional<RecentTasksController> recentTasksOptional,
+ Optional<WindowDecorViewModel<?>> windowDecorViewModelOptional) {
if (fullscreenTaskListener.isPresent()) {
return fullscreenTaskListener.get();
} else {
return new FullscreenTaskListener(shellInit, shellTaskOrganizer, syncQueue,
- recentTasksOptional);
+ recentTasksOptional, windowDecorViewModelOptional);
}
}
//
+ // Window Decoration
+ //
+
+ @BindsOptionalOf
+ abstract WindowDecorViewModel<?> optionalWindowDecorViewModel();
+
+ //
// Unfold transition
//
@@ -627,11 +636,12 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static ActivityEmbeddingController provideActivityEmbeddingController(
+ static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
Context context,
ShellInit shellInit,
Transitions transitions) {
- return new ActivityEmbeddingController(context, shellInit, transitions);
+ return Optional.ofNullable(
+ ActivityEmbeddingController.create(context, shellInit, transitions));
}
//
@@ -679,14 +689,14 @@ public abstract class WMShellBaseModule {
Optional<SplitScreenController> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
- FullscreenTaskListener fullscreenTaskListener,
+ FullscreenTaskListener<?> fullscreenTaskListener,
Optional<UnfoldAnimationController> unfoldAnimationController,
Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
Optional<FreeformComponents> freeformComponents,
Optional<RecentTasksController> recentTasksOptional,
Optional<OneHandedController> oneHandedControllerOptional,
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
- ActivityEmbeddingController activityEmbeddingOptional,
+ Optional<ActivityEmbeddingController> activityEmbeddingOptional,
Transitions transitions,
StartingWindowController startingWindow,
@ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2bcc134f9dd5..c64d1134a4ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -55,6 +55,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
@@ -145,6 +146,7 @@ public abstract class WMShellModule {
@Provides
static BubbleController provideBubbleController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
BubbleData data,
FloatingContentCoordinator floatingContentCoordinator,
@@ -165,7 +167,7 @@ public abstract class WMShellModule {
@ShellBackgroundThread ShellExecutor bgExecutor,
TaskViewTransitions taskViewTransitions,
SyncTransactionQueue syncQueue) {
- return new BubbleController(context, shellInit, shellController, data,
+ return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
null /* synchronizer */, floatingContentCoordinator,
new BubbleDataRepository(context, launcherApps, mainExecutor),
statusBarService, windowManager, windowManagerShellWrapper, userManager,
@@ -232,6 +234,7 @@ public abstract class WMShellModule {
ShellInit shellInit,
Transitions transitions,
WindowDecorViewModel<?> windowDecorViewModel,
+ FullscreenTaskListener<?> fullscreenTaskListener,
FreeformTaskListener<?> freeformTaskListener) {
// TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
// override for this controller from the base module
@@ -239,7 +242,7 @@ public abstract class WMShellModule {
? shellInit
: null;
return new FreeformTaskTransitionHandler(init, transitions,
- windowDecorViewModel, freeformTaskListener);
+ windowDecorViewModel, fullscreenTaskListener, freeformTaskListener);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index ab66107399c6..8dcdda1895e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -187,12 +187,14 @@ public class FreeformTaskListener<T extends AutoCloseable>
RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- T windowDecor = mWindowDecorOfVanishedTasks.get(taskInfo.taskId);
- mWindowDecorOfVanishedTasks.remove(taskInfo.taskId);
+ T windowDecor;
final State<T> state = mTasks.get(taskInfo.taskId);
if (state != null) {
- windowDecor = windowDecor == null ? state.mWindowDecoration : windowDecor;
+ windowDecor = state.mWindowDecoration;
state.mWindowDecoration = null;
+ } else {
+ windowDecor =
+ mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
}
mWindowDecorationViewModel.setupWindowDecorationForTransition(
taskInfo, startT, finishT, windowDecor);
@@ -231,7 +233,8 @@ public class FreeformTaskListener<T extends AutoCloseable>
if (mWindowDecorOfVanishedTasks.size() == 0) {
return;
}
- Log.w(TAG, "Clearing window decors of vanished tasks. There could be visual defects "
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Clearing window decors of vanished tasks. There could be visual defects "
+ "if any of them is used later in transitions.");
for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) {
releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index a1e9f938d280..30f625e24118 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -32,6 +32,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -50,6 +51,7 @@ public class FreeformTaskTransitionHandler
private final Transitions mTransitions;
private final FreeformTaskListener<?> mFreeformTaskListener;
+ private final FullscreenTaskListener<?> mFullscreenTaskListener;
private final WindowDecorViewModel<?> mWindowDecorViewModel;
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
@@ -58,8 +60,10 @@ public class FreeformTaskTransitionHandler
ShellInit shellInit,
Transitions transitions,
WindowDecorViewModel<?> windowDecorViewModel,
+ FullscreenTaskListener<?> fullscreenTaskListener,
FreeformTaskListener<?> freeformTaskListener) {
mTransitions = transitions;
+ mFullscreenTaskListener = fullscreenTaskListener;
mFreeformTaskListener = freeformTaskListener;
mWindowDecorViewModel = windowDecorViewModel;
if (shellInit != null && Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -150,10 +154,16 @@ public class FreeformTaskTransitionHandler
TransitionInfo.Change change,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- if (change.getTaskInfo().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
- return false;
+ switch (change.getTaskInfo().getWindowingMode()){
+ case WINDOWING_MODE_FREEFORM:
+ mFreeformTaskListener.createWindowDecoration(change, startT, finishT);
+ break;
+ case WINDOWING_MODE_FULLSCREEN:
+ mFullscreenTaskListener.createWindowDecoration(change, startT, finishT);
+ break;
+ default:
+ return false;
}
- mFreeformTaskListener.createWindowDecoration(change, startT, finishT);
// Intercepted transition to manage the window decorations. Let other handlers animate.
return false;
@@ -164,15 +174,22 @@ public class FreeformTaskTransitionHandler
ArrayList<AutoCloseable> windowDecors,
SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT) {
- if (change.getTaskInfo().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
- return false;
+ final AutoCloseable windowDecor;
+ switch (change.getTaskInfo().getWindowingMode()) {
+ case WINDOWING_MODE_FREEFORM:
+ windowDecor = mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(),
+ startT, finishT);
+ break;
+ case WINDOWING_MODE_FULLSCREEN:
+ windowDecor = mFullscreenTaskListener.giveWindowDecoration(change.getTaskInfo(),
+ startT, finishT);
+ break;
+ default:
+ windowDecor = null;
}
- final AutoCloseable windowDecor =
- mFreeformTaskListener.giveWindowDecoration(change.getTaskInfo(), startT, finishT);
if (windowDecor != null) {
windowDecors.add(windowDecor);
}
-
// Intercepted transition to manage the window decorations. Let other handlers animate.
return false;
}
@@ -197,24 +214,29 @@ public class FreeformTaskTransitionHandler
}
boolean handled = false;
+ boolean adopted = false;
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (type == Transitions.TRANSIT_MAXIMIZE
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
handled = true;
windowDecor = mFreeformTaskListener.giveWindowDecoration(
change.getTaskInfo(), startT, finishT);
- // TODO(b/235638450): Let fullscreen task listener adopt the window decor.
+ adopted = mFullscreenTaskListener.adoptWindowDecoration(change,
+ startT, finishT, windowDecor);
}
if (type == Transitions.TRANSIT_RESTORE_FROM_MAXIMIZE
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
handled = true;
- // TODO(b/235638450): Let fullscreen task listener transfer the window decor.
- mFreeformTaskListener.adoptWindowDecoration(change, startT, finishT, windowDecor);
+ windowDecor = mFullscreenTaskListener.giveWindowDecoration(
+ change.getTaskInfo(), startT, finishT);
+ adopted = mFreeformTaskListener.adoptWindowDecoration(change,
+ startT, finishT, windowDecor);
}
- releaseWindowDecor(windowDecor);
-
+ if (!adopted) {
+ releaseWindowDecor(windowDecor);
+ }
return handled;
}
@@ -225,7 +247,7 @@ public class FreeformTaskTransitionHandler
releaseWindowDecor(windowDecor);
}
mFreeformTaskListener.onTaskTransitionFinished();
- // TODO(b/235638450): Dispatch it to fullscreen task listener.
+ mFullscreenTaskListener.onTaskTransitionFinished();
finishCallback.onTransitionFinished(null, null);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 0ba4afc24c45..0d75bc451b72 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -16,16 +16,21 @@
package com.android.wm.shell.fullscreen;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
+import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.graphics.Point;
-import android.util.Slog;
+import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.window.TransitionInfo;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -34,36 +39,49 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.io.PrintWriter;
import java.util.Optional;
/**
* Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}.
+ * @param <T> the type of window decoration instance
*/
-public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
+public class FullscreenTaskListener<T extends AutoCloseable>
+ implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "FullscreenTaskListener";
private final ShellTaskOrganizer mShellTaskOrganizer;
- private final SyncTransactionQueue mSyncQueue;
- private final Optional<RecentTasksController> mRecentTasksOptional;
- private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
+ private final SparseArray<State<T>> mTasks = new SparseArray<>();
+ private final SparseArray<T> mWindowDecorOfVanishedTasks = new SparseArray<>();
+ private static class State<T extends AutoCloseable> {
+ RunningTaskInfo mTaskInfo;
+ SurfaceControl mLeash;
+ T mWindowDecoration;
+ }
+ private final SyncTransactionQueue mSyncQueue;
+ private final Optional<RecentTasksController> mRecentTasksOptional;
+ private final Optional<WindowDecorViewModel<T>> mWindowDecorViewModelOptional;
/**
* This constructor is used by downstream products.
*/
public FullscreenTaskListener(SyncTransactionQueue syncQueue) {
- this(null /* shellInit */, null /* shellTaskOrganizer */, syncQueue, Optional.empty());
+ this(null /* shellInit */, null /* shellTaskOrganizer */, syncQueue, Optional.empty(),
+ Optional.empty());
}
public FullscreenTaskListener(ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
SyncTransactionQueue syncQueue,
- Optional<RecentTasksController> recentTasksOptional) {
+ Optional<RecentTasksController> recentTasksOptional,
+ Optional<WindowDecorViewModel<T>> windowDecorViewModelOptional) {
mShellTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
mRecentTasksOptional = recentTasksOptional;
+ mWindowDecorViewModelOptional = windowDecorViewModelOptional;
// Note: Some derivative FullscreenTaskListener implementations do not use ShellInit
if (shellInit != null) {
shellInit.addInitCallback(this::onInit, this);
@@ -76,55 +94,204 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
- if (mDataByTaskId.get(taskInfo.taskId) != null) {
+ if (mTasks.get(taskInfo.taskId) != null) {
throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId);
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d",
taskInfo.taskId);
final Point positionInParent = taskInfo.positionInParent;
- mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent));
-
- if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
- mSyncQueue.runInSync(t -> {
- // Reset several properties back to fullscreen (PiP, for example, leaves all these
- // properties in a bad state).
- t.setWindowCrop(leash, null);
- t.setPosition(leash, positionInParent.x, positionInParent.y);
- t.setAlpha(leash, 1f);
- t.setMatrix(leash, 1, 0, 0, 1);
- t.show(leash);
- });
+ final State<T> state = new State();
+ state.mLeash = leash;
+ state.mTaskInfo = taskInfo;
+ mTasks.put(taskInfo.taskId, state);
updateRecentsForVisibleFullscreenTask(taskInfo);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
+ if (shouldShowWindowDecor(taskInfo) && mWindowDecorViewModelOptional.isPresent()) {
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ state.mWindowDecoration =
+ mWindowDecorViewModelOptional.get().createWindowDecoration(taskInfo,
+ leash, t, t);
+ t.apply();
+ } else {
+ mSyncQueue.runInSync(t -> {
+ // Reset several properties back to fullscreen (PiP, for example, leaves all these
+ // properties in a bad state).
+ t.setWindowCrop(leash, null);
+ t.setPosition(leash, positionInParent.x, positionInParent.y);
+ t.setAlpha(leash, 1f);
+ t.setMatrix(leash, 1, 0, 0, 1);
+ t.show(leash);
+ });
+ }
}
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
- if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
-
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ final Point oldPositionInParent = state.mTaskInfo.positionInParent;
+ state.mTaskInfo = taskInfo;
+ if (state.mWindowDecoration != null) {
+ mWindowDecorViewModelOptional.get().onTaskInfoChanged(
+ state.mTaskInfo, state.mWindowDecoration);
+ }
updateRecentsForVisibleFullscreenTask(taskInfo);
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
- final TaskData data = mDataByTaskId.get(taskInfo.taskId);
- final Point positionInParent = taskInfo.positionInParent;
- if (!positionInParent.equals(data.positionInParent)) {
- data.positionInParent.set(positionInParent.x, positionInParent.y);
+ final Point positionInParent = state.mTaskInfo.positionInParent;
+ if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) {
mSyncQueue.runInSync(t -> {
- t.setPosition(data.surface, positionInParent.x, positionInParent.y);
+ t.setPosition(state.mLeash, positionInParent.x, positionInParent.y);
});
}
}
@Override
- public void onTaskVanished(RunningTaskInfo taskInfo) {
- if (mDataByTaskId.get(taskInfo.taskId) == null) {
- Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
+ public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ if (state == null) {
+ // This is possible if the transition happens before this method.
return;
}
-
- mDataByTaskId.remove(taskInfo.taskId);
-
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
taskInfo.taskId);
+ mTasks.remove(taskInfo.taskId);
+
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ // Save window decorations of closing tasks so that we can hand them over to the
+ // transition system if this method happens before the transition. In case where the
+ // transition didn't happen, it'd be cleared when the next transition finished.
+ if (state.mWindowDecoration != null) {
+ mWindowDecorOfVanishedTasks.put(taskInfo.taskId, state.mWindowDecoration);
+ }
+ return;
+ }
+ releaseWindowDecor(state.mWindowDecoration);
+ }
+
+ /**
+ * Creates a window decoration for a transition.
+ *
+ * @param change the change of this task transition that needs to have the task layer as the
+ * leash
+ */
+ public void createWindowDecoration(TransitionInfo.Change change,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
+ final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
+ if (!mWindowDecorViewModelOptional.isPresent()
+ || !shouldShowWindowDecor(state.mTaskInfo)) {
+ return;
+ }
+
+ state.mWindowDecoration = mWindowDecorViewModelOptional.get().createWindowDecoration(
+ state.mTaskInfo, state.mLeash, startT, finishT);
+ }
+
+ /**
+ * Adopt the incoming window decoration and lets the window decoration prepare for a transition.
+ *
+ * @param change the change of this task transition that needs to have the task layer as the
+ * leash
+ * @param startT the start transaction of this transition
+ * @param finishT the finish transaction of this transition
+ * @param windowDecor the window decoration to adopt
+ * @return {@code true} if it adopts the window decoration; {@code false} otherwise
+ */
+ public boolean adoptWindowDecoration(
+ TransitionInfo.Change change,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT,
+ @Nullable AutoCloseable windowDecor) {
+ if (!mWindowDecorViewModelOptional.isPresent()
+ || !shouldShowWindowDecor(change.getTaskInfo())) {
+ return false;
+ }
+ final State<T> state = createOrUpdateTaskState(change.getTaskInfo(), change.getLeash());
+ state.mWindowDecoration = mWindowDecorViewModelOptional.get().adoptWindowDecoration(
+ windowDecor);
+ if (state.mWindowDecoration != null) {
+ mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
+ state.mTaskInfo, startT, finishT, state.mWindowDecoration);
+ return true;
+ } else {
+ state.mWindowDecoration = mWindowDecorViewModelOptional.get().createWindowDecoration(
+ state.mTaskInfo, state.mLeash, startT, finishT);
+ return false;
+ }
+ }
+
+ /**
+ * Clear window decors of vanished tasks.
+ */
+ public void onTaskTransitionFinished() {
+ if (mWindowDecorOfVanishedTasks.size() == 0) {
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Clearing window decors of vanished tasks. There could be visual defects "
+ + "if any of them is used later in transitions.");
+ for (int i = 0; i < mWindowDecorOfVanishedTasks.size(); ++i) {
+ releaseWindowDecor(mWindowDecorOfVanishedTasks.valueAt(i));
+ }
+ mWindowDecorOfVanishedTasks.clear();
+ }
+
+ /**
+ * Gives out the ownership of the task's window decoration. The given task is leaving (of has
+ * left) this task listener. This is the transition system asking for the ownership.
+ *
+ * @param taskInfo the maximizing task
+ * @return the window decor of the maximizing task if any
+ */
+ public T giveWindowDecoration(
+ ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT) {
+ T windowDecor;
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ if (state != null) {
+ windowDecor = state.mWindowDecoration;
+ state.mWindowDecoration = null;
+ } else {
+ windowDecor =
+ mWindowDecorOfVanishedTasks.removeReturnOld(taskInfo.taskId);
+ }
+ if (mWindowDecorViewModelOptional.isPresent()) {
+ mWindowDecorViewModelOptional.get().setupWindowDecorationForTransition(
+ taskInfo, startT, finishT, windowDecor);
+ }
+
+ return windowDecor;
+ }
+
+ private State<T> createOrUpdateTaskState(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl leash) {
+ State<T> state = mTasks.get(taskInfo.taskId);
+ if (state != null) {
+ updateTaskInfo(taskInfo);
+ return state;
+ }
+
+ state = new State<T>();
+ state.mTaskInfo = taskInfo;
+ state.mLeash = leash;
+ mTasks.put(taskInfo.taskId, state);
+
+ return state;
+ }
+
+ private State<T> updateTaskInfo(ActivityManager.RunningTaskInfo taskInfo) {
+ final State<T> state = mTasks.get(taskInfo.taskId);
+ state.mTaskInfo = taskInfo;
+ return state;
+ }
+
+ private void releaseWindowDecor(T windowDecor) {
+ try {
+ windowDecor.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to release window decoration.", e);
+ }
}
private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) {
@@ -148,17 +315,17 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
}
private SurfaceControl findTaskSurface(int taskId) {
- if (!mDataByTaskId.contains(taskId)) {
+ if (!mTasks.contains(taskId)) {
throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
}
- return mDataByTaskId.get(taskId).surface;
+ return mTasks.get(taskId).mLeash;
}
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + this);
- pw.println(innerPrefix + mDataByTaskId.size() + " Tasks");
+ pw.println(innerPrefix + mTasks.size() + " Tasks");
}
@Override
@@ -166,16 +333,10 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN);
}
- /**
- * Per-task data for each managed task.
- */
- private static class TaskData {
- public final SurfaceControl surface;
- public final Point positionInParent;
-
- public TaskData(SurfaceControl surface, Point positionInParent) {
- this.surface = surface;
- this.positionInParent = positionInParent;
- }
+ private static boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
+ return taskInfo.getConfiguration().windowConfiguration.getDisplayWindowingMode()
+ == WINDOWING_MODE_FREEFORM;
}
+
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
index 76c0f41997ad..7129165a78dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -37,16 +37,6 @@ public interface OneHanded {
}
/**
- * Return one handed settings enabled or not.
- */
- boolean isOneHandedEnabled();
-
- /**
- * Return swipe to notification settings enabled or not.
- */
- boolean isSwipeToNotificationEnabled();
-
- /**
* Enters one handed mode.
*/
void startOneHanded();
@@ -80,9 +70,4 @@ public interface OneHanded {
* transition start or finish
*/
void registerTransitionCallback(OneHandedTransitionCallback callback);
-
- /**
- * Notifies when user switch complete
- */
- void onUserSwitch(int userId);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 9149204b94ce..e0c4fe8c4fba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -59,6 +59,7 @@ import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import java.io.PrintWriter;
@@ -67,7 +68,7 @@ import java.io.PrintWriter;
*/
public class OneHandedController implements RemoteCallable<OneHandedController>,
DisplayChangeController.OnDisplayChangingListener, ConfigurationChangeListener,
- KeyguardChangeListener {
+ KeyguardChangeListener, UserChangeListener {
private static final String TAG = "OneHandedController";
private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE =
@@ -76,8 +77,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
- private volatile boolean mIsOneHandedEnabled;
- private volatile boolean mIsSwipeToNotificationEnabled;
+ private boolean mIsOneHandedEnabled;
+ private boolean mIsSwipeToNotificationEnabled;
private boolean mIsShortcutEnabled;
private boolean mTaskChangeToExit;
private boolean mLockedDisabled;
@@ -294,6 +295,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
mState.addSListeners(mTutorialHandler);
mShellController.addConfigurationChangeListener(this);
mShellController.addKeyguardChangeListener(this);
+ mShellController.addUserChangeListener(this);
}
public OneHanded asOneHanded() {
@@ -627,7 +629,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
stopOneHanded();
}
- private void onUserSwitch(int newUserId) {
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
unregisterSettingObservers();
mUserId = newUserId;
registerSettingObservers(newUserId);
@@ -718,18 +721,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
}
@Override
- public boolean isOneHandedEnabled() {
- // This is volatile so return directly
- return mIsOneHandedEnabled;
- }
-
- @Override
- public boolean isSwipeToNotificationEnabled() {
- // This is volatile so return directly
- return mIsSwipeToNotificationEnabled;
- }
-
- @Override
public void startOneHanded() {
mMainExecutor.execute(() -> {
OneHandedController.this.startOneHanded();
@@ -770,13 +761,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
OneHandedController.this.registerTransitionCallback(callback);
});
}
-
- @Override
- public void onUserSwitch(int userId) {
- mMainExecutor.execute(() -> {
- OneHandedController.this.onUserSwitch(userId);
- });
- }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index 93172f82edd1..c06881ae6ad7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -51,12 +51,6 @@ public interface Pip {
}
/**
- * Registers the session listener for the current user.
- */
- default void registerSessionListenerForCurrentUser() {
- }
-
- /**
* Sets both shelf visibility and its height.
*
* @param visible visibility of shelf.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index fc97f310ad4e..ac3407dd1ca1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -92,6 +92,7 @@ import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
@@ -105,7 +106,8 @@ import java.util.function.Consumer;
* Manages the picture-in-picture (PIP) UI and states for Phones.
*/
public class PipController implements PipTransitionController.PipTransitionCallback,
- RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener {
+ RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener,
+ UserChangeListener {
private static final String TAG = "PipController";
private Context mContext;
@@ -528,7 +530,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
});
mOneHandedController.ifPresent(controller -> {
- controller.asOneHanded().registerTransitionCallback(
+ controller.registerTransitionCallback(
new OneHandedTransitionCallback() {
@Override
public void onStartFinished(Rect bounds) {
@@ -542,8 +544,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb
});
});
+ mMediaController.registerSessionListenerForCurrentUser();
+
mShellController.addConfigurationChangeListener(this);
mShellController.addKeyguardChangeListener(this);
+ mShellController.addUserChangeListener(this);
}
@Override
@@ -557,6 +562,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ // Re-register the media session listener when switching users
+ mMediaController.registerSessionListenerForCurrentUser();
+ }
+
+ @Override
public void onConfigurationChanged(Configuration newConfig) {
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
mTouchHandler.onConfigurationChanged();
@@ -644,10 +655,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
}
- private void registerSessionListenerForCurrentUser() {
- mMediaController.registerSessionListenerForCurrentUser();
- }
-
private void onSystemUiStateChanged(boolean isValidState, int flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
@@ -968,13 +975,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void registerSessionListenerForCurrentUser() {
- mMainExecutor.execute(() -> {
- PipController.this.registerSessionListenerForCurrentUser();
- });
- }
-
- @Override
public void setShelfHeight(boolean visible, int height) {
mMainExecutor.execute(() -> {
PipController.this.setShelfHeight(visible, height);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index a24d9618032d..4e1b0469eb96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -32,6 +32,8 @@ import android.graphics.Rect;
import android.os.RemoteException;
import android.view.Gravity;
+import androidx.annotation.NonNull;
+
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.WindowManagerShellWrapper;
@@ -51,6 +53,8 @@ import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.sysui.UserChangeListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -64,7 +68,7 @@ import java.util.Set;
public class TvPipController implements PipTransitionController.PipTransitionCallback,
TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener,
- ConfigurationChangeListener {
+ ConfigurationChangeListener, UserChangeListener {
private static final String TAG = "TvPipController";
static final boolean DEBUG = false;
@@ -105,6 +109,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private final PipMediaController mPipMediaController;
private final TvPipNotificationController mPipNotificationController;
private final TvPipMenuController mTvPipMenuController;
+ private final PipTransitionController mPipTransitionController;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private final PipParamsChangedForwarder mPipParamsChangedForwarder;
+ private final DisplayController mDisplayController;
+ private final WindowManagerShellWrapper mWmShellWrapper;
private final ShellExecutor mMainExecutor;
private final TvPipImpl mImpl = new TvPipImpl();
@@ -121,6 +130,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
public static Pip create(
Context context,
+ ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -138,6 +148,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
ShellExecutor mainExecutor) {
return new TvPipController(
context,
+ shellInit,
shellController,
tvPipBoundsState,
tvPipBoundsAlgorithm,
@@ -157,6 +168,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
private TvPipController(
Context context,
+ ShellInit shellInit,
ShellController shellController,
TvPipBoundsState tvPipBoundsState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
@@ -170,11 +182,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
- WindowManagerShellWrapper wmShell,
+ WindowManagerShellWrapper wmShellWrapper,
ShellExecutor mainExecutor) {
mContext = context;
mMainExecutor = mainExecutor;
mShellController = shellController;
+ mDisplayController = displayController;
mTvPipBoundsState = tvPipBoundsState;
mTvPipBoundsState.setDisplayId(context.getDisplayId());
@@ -193,16 +206,32 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
mAppOpsListener = pipAppOpsListener;
mPipTaskOrganizer = pipTaskOrganizer;
- pipTransitionController.registerPipTransitionCallback(this);
+ mPipTransitionController = pipTransitionController;
+ mPipParamsChangedForwarder = pipParamsChangedForwarder;
+ mTaskStackListener = taskStackListener;
+ mWmShellWrapper = wmShellWrapper;
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mPipTransitionController.registerPipTransitionCallback(this);
loadConfigurations();
- registerPipParamsChangedListener(pipParamsChangedForwarder);
- registerTaskStackListenerCallback(taskStackListener);
- registerWmShellPinnedStackListener(wmShell);
- displayController.addDisplayWindowListener(this);
+ registerPipParamsChangedListener(mPipParamsChangedForwarder);
+ registerTaskStackListenerCallback(mTaskStackListener);
+ registerWmShellPinnedStackListener(mWmShellWrapper);
+ registerSessionListenerForCurrentUser();
+ mDisplayController.addDisplayWindowListener(this);
mShellController.addConfigurationChangeListener(this);
+ mShellController.addUserChangeListener(this);
+ }
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ // Re-register the media session listener when switching users
+ registerSessionListenerForCurrentUser();
}
@Override
@@ -679,11 +708,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal
}
private class TvPipImpl implements Pip {
- @Override
- public void registerSessionListenerForCurrentUser() {
- mMainExecutor.execute(() -> {
- TvPipController.this.registerSessionListenerForCurrentUser();
- });
- }
+ // Not used
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 7e83d2fa0a0b..21fc01e554c8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -488,13 +489,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final WindowContainerTransaction wct = new WindowContainerTransaction();
options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
- // If split still not active, apply windows bounds first to avoid surface reset to
- // wrong pos by SurfaceAnimator from wms.
- // TODO(b/223325631): check is it still necessary after improve enter transition done.
- if (!mMainStage.isActive()) {
- updateWindowBounds(mSplitLayout, wct);
- }
-
wct.sendPendingIntent(intent, fillInIntent, options);
mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
}
@@ -641,7 +635,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
wct.startTask(sideTaskId, sideOptions);
}
// Using legacy transitions, so we can't use blast sync since it conflicts.
- mTaskOrganizer.applyTransaction(wct);
+ mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
setDividerVisibility(true, t);
updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
@@ -893,10 +887,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mShouldUpdateRecents = false;
mIsDividerRemoteAnimating = false;
+ mSplitLayout.getInvisibleBounds(mTempRect1);
if (childrenToTop == null) {
mSideStage.removeAllTasks(wct, false /* toTop */);
mMainStage.deactivate(wct, false /* toTop */);
wct.reorder(mRootTaskInfo.token, false /* onTop */);
+ wct.setForceTranslucent(mRootTaskInfo.token, true);
+ wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
onTransitionAnimationComplete();
} else {
// Expand to top side split as full screen for fading out decor animation and dismiss
@@ -907,27 +904,32 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? mSideStage : mMainStage;
tempFullStage.resetBounds(wct);
wct.setSmallestScreenWidthDp(tempFullStage.mRootTaskInfo.token,
- mRootTaskInfo.configuration.smallestScreenWidthDp);
+ SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
dismissStage.dismiss(wct, false /* toTop */);
}
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
t.setWindowCrop(mMainStage.mRootLeash, null)
.setWindowCrop(mSideStage.mRootLeash, null);
- t.setPosition(mMainStage.mRootLeash, 0, 0)
- .setPosition(mSideStage.mRootLeash, 0, 0);
t.hide(mMainStage.mDimLayer).hide(mSideStage.mDimLayer);
setDividerVisibility(false, t);
- // In this case, exit still under progress, fade out the split decor after first WCT
- // done and do remaining WCT after animation finished.
- if (childrenToTop != null) {
+ if (childrenToTop == null) {
+ t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
+ } else {
+ // In this case, exit still under progress, fade out the split decor after first WCT
+ // done and do remaining WCT after animation finished.
childrenToTop.fadeOutDecor(() -> {
WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
mIsExiting = false;
childrenToTop.dismiss(finishedWCT, true /* toTop */);
finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
- mTaskOrganizer.applyTransaction(finishedWCT);
+ finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
+ finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+ mSyncQueue.queue(finishedWCT);
+ mSyncQueue.runInSync(at -> {
+ at.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
+ });
onTransitionAnimationComplete();
});
}
@@ -996,6 +998,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mMainStage.activate(wct, true /* includingTopTask */);
updateWindowBounds(mSplitLayout, wct);
wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
}
void finishEnterSplitScreen(SurfaceControl.Transaction t) {
@@ -1221,7 +1224,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Make the stages adjacent to each other so they occlude what's behind them.
wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
- mTaskOrganizer.applyTransaction(wct);
+ wct.setForceTranslucent(mRootTaskInfo.token, true);
+ mSplitLayout.getInvisibleBounds(mTempRect1);
+ wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+ mSyncQueue.queue(wct);
+ mSyncQueue.runInSync(t -> {
+ t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
+ });
}
private void onRootTaskVanished() {
@@ -1377,10 +1386,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// TODO (b/238697912) : Add the validation to prevent entering non-recovered status
final WindowContainerTransaction wct = new WindowContainerTransaction();
mSplitLayout.init();
- prepareEnterSplitScreen(wct);
+ mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+ mMainStage.activate(wct, true /* includingTopTask */);
+ updateWindowBounds(mSplitLayout, wct);
+ wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
mSyncQueue.queue(wct);
- mSyncQueue.runInSync(t ->
- updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */));
+ mSyncQueue.runInSync(t -> {
+ updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+
+ mSplitLayout.flingDividerToCenter();
+ });
}
if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
mShouldUpdateRecents = true;
@@ -1822,6 +1838,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// properly for the animation itself.
mSplitLayout.release();
mSplitLayout.resetDividerPosition();
+ mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
index 1c0b35894acd..9df863163b50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/KeyguardChangeListener.java
@@ -21,13 +21,13 @@ package com.android.wm.shell.sysui;
*/
public interface KeyguardChangeListener {
/**
- * Notifies the Shell that the keyguard is showing (and if so, whether it is occluded).
+ * Called when the keyguard is showing (and if so, whether it is occluded).
*/
default void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
boolean animatingDismiss) {}
/**
- * Notifies the Shell when the keyguard dismiss animation has finished.
+ * Called when the keyguard dismiss animation has finished.
*
* TODO(b/206741900) deprecate this path once we're able to animate the PiP window as part of
* keyguard dismiss animation.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index 52ffb46bb39c..57993948886b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -25,7 +25,9 @@ import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SYSUI_EVENTS;
+import android.content.Context;
import android.content.pm.ActivityInfo;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
@@ -36,6 +38,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ExternalThread;
import java.io.PrintWriter;
+import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -53,6 +56,9 @@ public class ShellController {
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners =
new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
+ new CopyOnWriteArrayList<>();
+
private Configuration mLastConfiguration;
@@ -102,6 +108,22 @@ public class ShellController {
mKeyguardChangeListeners.remove(listener);
}
+ /**
+ * Adds a new user-change listener. The user change callbacks are not made in any
+ * particular order.
+ */
+ public void addUserChangeListener(UserChangeListener listener) {
+ mUserChangeListeners.remove(listener);
+ mUserChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing user-change listener.
+ */
+ public void removeUserChangeListener(UserChangeListener listener) {
+ mUserChangeListeners.remove(listener);
+ }
+
@VisibleForTesting
void onConfigurationChanged(Configuration newConfig) {
// The initial config is send on startup and doesn't trigger listener callbacks
@@ -144,6 +166,8 @@ public class ShellController {
@VisibleForTesting
void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard visibility changed: visible=%b "
+ + "occluded=%b animatingDismiss=%b", visible, occluded, animatingDismiss);
for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
listener.onKeyguardVisibilityChanged(visible, occluded, animatingDismiss);
}
@@ -151,17 +175,35 @@ public class ShellController {
@VisibleForTesting
void onKeyguardDismissAnimationFinished() {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Keyguard dismiss animation finished");
for (KeyguardChangeListener listener : mKeyguardChangeListeners) {
listener.onKeyguardDismissAnimationFinished();
}
}
+ @VisibleForTesting
+ void onUserChanged(int newUserId, @NonNull Context userContext) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User changed: id=%d", newUserId);
+ for (UserChangeListener listener : mUserChangeListeners) {
+ listener.onUserChanged(newUserId, userContext);
+ }
+ }
+
+ @VisibleForTesting
+ void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "User profiles changed");
+ for (UserChangeListener listener : mUserChangeListeners) {
+ listener.onUserProfilesChanged(profiles);
+ }
+ }
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "mConfigChangeListeners=" + mConfigChangeListeners.size());
pw.println(innerPrefix + "mLastConfiguration=" + mLastConfiguration);
pw.println(innerPrefix + "mKeyguardChangeListeners=" + mKeyguardChangeListeners.size());
+ pw.println(innerPrefix + "mUserChangeListeners=" + mUserChangeListeners.size());
}
/**
@@ -220,5 +262,17 @@ public class ShellController {
mMainExecutor.execute(() ->
ShellController.this.onKeyguardDismissAnimationFinished());
}
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onUserChanged(newUserId, userContext));
+ }
+
+ @Override
+ public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ mMainExecutor.execute(() ->
+ ShellController.this.onUserProfilesChanged(profiles));
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
index 254c253b0042..2108c824ac6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
@@ -16,9 +16,14 @@
package com.android.wm.shell.sysui;
+import android.content.Context;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
+import androidx.annotation.NonNull;
+
import java.io.PrintWriter;
+import java.util.List;
/**
* General interface for notifying the Shell of common SysUI events like configuration or keyguard
@@ -59,4 +64,14 @@ public interface ShellInterface {
* Notifies the Shell when the keyguard dismiss animation has finished.
*/
default void onKeyguardDismissAnimationFinished() {}
+
+ /**
+ * Notifies the Shell when the user changes.
+ */
+ default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+ /**
+ * Notifies the Shell when a profile belonging to the user changes.
+ */
+ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
new file mode 100644
index 000000000000..3d0909f6128d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/UserChangeListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.sysui;
+
+import android.content.Context;
+import android.content.pm.UserInfo;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Callbacks for when the user or user's profiles changes.
+ */
+public interface UserChangeListener {
+ /**
+ * Called when the current (parent) user changes.
+ */
+ default void onUserChanged(int newUserId, @NonNull Context userContext) {}
+
+ /**
+ * Called when a profile belonging to the user changes.
+ */
+ default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 26d0ec637ccf..29d25bc39223 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -219,6 +219,8 @@ public class Transitions implements RemoteCallable<Transitions> {
+ "use ShellInit callbacks to ensure proper ordering");
}
mHandlers.add(handler);
+ // Set initial scale settings.
+ handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
handler.getClass().getSimpleName());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index dc3deb1a927c..8b13721ef428 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -142,7 +142,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
return;
}
- if (oldDecorationSurface != mDecorationContainerSurface) {
+ if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
closeDragResizeListener();
mDragResizeListener = new DragResizeInputListener(
mContext,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
index 32f1587752cb..ff1d2990a82a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java
@@ -169,6 +169,7 @@ public class TaskViewTest extends ShellTestCase {
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
+ assertThat(mTaskView.isInitialized()).isTrue();
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
}
@@ -178,6 +179,7 @@ public class TaskViewTest extends ShellTestCase {
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
verify(mViewListener).onInitialized();
+ assertThat(mTaskView.isInitialized()).isTrue();
// No task, no visibility change
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
}
@@ -189,6 +191,7 @@ public class TaskViewTest extends ShellTestCase {
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
verify(mViewListener).onInitialized();
+ assertThat(mTaskView.isInitialized()).isTrue();
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true));
}
@@ -223,6 +226,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mOrganizer).removeListener(eq(mTaskView));
verify(mViewListener).onReleased();
+ assertThat(mTaskView.isInitialized()).isFalse();
}
@Test
@@ -270,6 +274,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
verify(mViewListener, never()).onInitialized();
+ assertThat(mTaskView.isInitialized()).isFalse();
// If there's no surface the task should be made invisible
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false));
}
@@ -281,6 +286,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mTaskViewTransitions, never()).setTaskViewVisible(any(), anyBoolean());
verify(mViewListener).onInitialized();
+ assertThat(mTaskView.isInitialized()).isTrue();
// No task, no visibility change
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
}
@@ -353,6 +359,7 @@ public class TaskViewTest extends ShellTestCase {
verify(mOrganizer).removeListener(eq(mTaskView));
verify(mViewListener).onReleased();
+ assertThat(mTaskView.isInitialized()).isFalse();
verify(mTaskViewTransitions).removeTaskView(eq(mTaskView));
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
new file mode 100644
index 000000000000..b2e45a6b3a5c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.activityembedding;
+
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.window.TransitionInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests for {@link ActivityEmbeddingAnimationRunner}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
+
+ @Before
+ public void setup() {
+ super.setUp();
+ doNothing().when(mController).onAnimationFinished(any());
+ }
+
+ @Test
+ public void testStartAnimation() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ info.addChange(embeddingChange);
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
+
+ mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
+
+ final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
+ verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
+ finishCallback.capture());
+ verify(mStartTransaction).apply();
+ verify(mAnimator).start();
+ verifyNoMoreInteractions(mFinishTransaction);
+ verify(mController, never()).onAnimationFinished(any());
+
+ // Call onAnimationFinished() when the animation is finished.
+ finishCallback.getValue().run();
+
+ verify(mController).onAnimationFinished(mTransition);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
new file mode 100644
index 000000000000..84befdddabdb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.activityembedding;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+
+import android.animation.Animator;
+import android.annotation.CallSuper;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** TestBase for ActivityEmbedding animation. */
+abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
+
+ @Mock
+ ShellInit mShellInit;
+ @Mock
+ Transitions mTransitions;
+ @Mock
+ IBinder mTransition;
+ @Mock
+ SurfaceControl.Transaction mStartTransaction;
+ @Mock
+ SurfaceControl.Transaction mFinishTransaction;
+ @Mock
+ Transitions.TransitionFinishCallback mFinishCallback;
+ @Mock
+ Animator mAnimator;
+
+ ActivityEmbeddingController mController;
+ ActivityEmbeddingAnimationRunner mAnimRunner;
+ ActivityEmbeddingAnimationSpec mAnimSpec;
+
+ @CallSuper
+ @Before
+ public void setUp() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+ MockitoAnnotations.initMocks(this);
+ mController = ActivityEmbeddingController.create(mContext, mShellInit, mTransitions);
+ assertNotNull(mController);
+ mAnimRunner = mController.mAnimationRunner;
+ assertNotNull(mAnimRunner);
+ mAnimSpec = mAnimRunner.mAnimationSpec;
+ assertNotNull(mAnimSpec);
+ spyOn(mController);
+ spyOn(mAnimRunner);
+ spyOn(mAnimSpec);
+ }
+
+ /** Creates a mock {@link TransitionInfo.Change}. */
+ static TransitionInfo.Change createChange() {
+ return new TransitionInfo.Change(mock(WindowContainerToken.class),
+ mock(SurfaceControl.class));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index bfe3b5468085..cf43b0030d2a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -16,52 +16,117 @@
package com.android.wm.shell.activityembedding;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
-import static org.junit.Assume.assumeTrue;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
-import android.content.Context;
+import android.window.TransitionInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import com.android.wm.shell.ShellTestCase;
-import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.Transitions;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
/**
- * Tests for the activity embedding controller.
+ * Tests for {@link ActivityEmbeddingController}.
*
* Build/Install/Run:
* atest WMShellUnitTests:ActivityEmbeddingControllerTests
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class ActivityEmbeddingControllerTests extends ShellTestCase {
-
- private @Mock Context mContext;
- private @Mock ShellInit mShellInit;
- private @Mock Transitions mTransitions;
- private ActivityEmbeddingController mController;
+public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
@Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- mController = spy(new ActivityEmbeddingController(mContext, mShellInit, mTransitions));
+ public void setup() {
+ super.setUp();
+ doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
}
@Test
- public void instantiate_addInitCallback() {
- assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
- verify(mShellInit, times(1)).addInitCallback(any(), any());
+ public void testInstantiate() {
+ verify(mShellInit).addInitCallback(any(), any());
+ }
+
+ @Test
+ public void testOnInit() {
+ mController.onInit();
+
+ verify(mTransitions).addHandler(mController);
+ }
+
+ @Test
+ public void testSetAnimScaleSetting() {
+ mController.setAnimScaleSetting(1.0f);
+
+ verify(mAnimRunner).setAnimScaleSetting(1.0f);
+ verify(mAnimSpec).setAnimScaleSetting(1.0f);
+ }
+
+ @Test
+ public void testStartAnimation_containsNonActivityEmbeddingChange() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ final TransitionInfo.Change nonEmbeddingChange = createChange();
+ info.addChange(embeddingChange);
+ info.addChange(nonEmbeddingChange);
+
+ // No-op
+ assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
+ verifyNoMoreInteractions(mStartTransaction);
+ verifyNoMoreInteractions(mFinishTransaction);
+ verifyNoMoreInteractions(mFinishCallback);
+ }
+
+ @Test
+ public void testStartAnimation_onlyActivityEmbeddingChange() {
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ info.addChange(embeddingChange);
+
+ // No-op
+ assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback));
+ verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction);
+ verify(mStartTransaction).apply();
+ verifyNoMoreInteractions(mFinishTransaction);
+ }
+
+ @Test
+ public void testOnAnimationFinished() {
+ // Should not call finish when there is no transition.
+ assertThrows(IllegalStateException.class,
+ () -> mController.onAnimationFinished(mTransition));
+
+ final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
+ final TransitionInfo.Change embeddingChange = createChange();
+ embeddingChange.setFlags(FLAG_IS_EMBEDDED);
+ info.addChange(embeddingChange);
+ mController.startAnimation(mTransition, info, mStartTransaction,
+ mFinishTransaction, mFinishCallback);
+
+ verify(mFinishCallback, never()).onTransitionFinished(any(), any());
+ mController.onAnimationFinished(mTransition);
+ verify(mFinishCallback).onTransitionFinished(any(), any());
+
+ // Should not call finish when the finish has already been called.
+ assertThrows(IllegalStateException.class,
+ () -> mController.onAnimationFinished(mTransition));
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 95725bbfd855..695550dd8fa5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -159,7 +159,8 @@ public class SplitLayoutTests extends ShellTestCase {
}
private void waitDividerFlingFinished() {
- verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture());
+ verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(),
+ mRunnableCaptor.capture());
mRunnableCaptor.getValue().run();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
index 90645ce4747d..cf8297eec061 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -171,6 +171,11 @@ public class OneHandedControllerTest extends OneHandedTestCase {
}
@Test
+ public void testControllerRegistersUserChangeListener() {
+ verify(mMockShellController, times(1)).addUserChangeListener(any());
+ }
+
+ @Test
public void testDefaultShouldNotInOneHanded() {
// Assert default transition state is STATE_NONE
assertThat(mSpiedTransitionState.getState()).isEqualTo(STATE_NONE);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 9ed8d84d665f..eb5726bebb74 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -77,9 +78,9 @@ import java.util.Set;
public class PipControllerTest extends ShellTestCase {
private PipController mPipController;
private ShellInit mShellInit;
+ private ShellController mShellController;
@Mock private ShellCommandHandler mMockShellCommandHandler;
- @Mock private ShellController mMockShellController;
@Mock private DisplayController mMockDisplayController;
@Mock private PhonePipMenuController mMockPhonePipMenuController;
@Mock private PipAppOpsListener mMockPipAppOpsListener;
@@ -110,8 +111,10 @@ public class PipControllerTest extends ShellTestCase {
return null;
}).when(mMockExecutor).execute(any());
mShellInit = spy(new ShellInit(mMockExecutor));
+ mShellController = spy(new ShellController(mShellInit, mMockShellCommandHandler,
+ mMockExecutor));
mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
- mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+ mShellController, mMockDisplayController, mMockPipAppOpsListener,
mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
@@ -135,12 +138,22 @@ public class PipControllerTest extends ShellTestCase {
@Test
public void instantiatePipController_registerConfigChangeListener() {
- verify(mMockShellController, times(1)).addConfigurationChangeListener(any());
+ verify(mShellController, times(1)).addConfigurationChangeListener(any());
}
@Test
public void instantiatePipController_registerKeyguardChangeListener() {
- verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
+ verify(mShellController, times(1)).addKeyguardChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerUserChangeListener() {
+ verify(mShellController, times(1)).addUserChangeListener(any());
+ }
+
+ @Test
+ public void instantiatePipController_registerMediaListener() {
+ verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
}
@Test
@@ -167,7 +180,7 @@ public class PipControllerTest extends ShellTestCase {
ShellInit shellInit = new ShellInit(mMockExecutor);
assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
- mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+ mShellController, mMockDisplayController, mMockPipAppOpsListener,
mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
@@ -264,4 +277,11 @@ public class PipControllerTest extends ShellTestCase {
verify(mMockPipBoundsState).setKeepClearAreas(Set.of(keepClearArea), Set.of());
}
+
+ @Test
+ public void onUserChangeRegisterMediaListener() {
+ reset(mMockPipMediaController);
+ mShellController.asShell().onUserChanged(100, mContext);
+ verify(mMockPipMediaController, times(1)).registerSessionListenerForCurrentUser();
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index 39e58ffcf9c7..d6ddba9e927d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -17,11 +17,15 @@
package com.android.wm.shell.sysui;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import android.content.Context;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -35,6 +39,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
@SmallTest
@@ -42,22 +48,29 @@ import java.util.Locale;
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class ShellControllerTest extends ShellTestCase {
+ private static final int TEST_USER_ID = 100;
+
@Mock
private ShellInit mShellInit;
@Mock
private ShellCommandHandler mShellCommandHandler;
@Mock
private ShellExecutor mExecutor;
+ @Mock
+ private Context mTestUserContext;
private ShellController mController;
private TestConfigurationChangeListener mConfigChangeListener;
private TestKeyguardChangeListener mKeyguardChangeListener;
+ private TestUserChangeListener mUserChangeListener;
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mKeyguardChangeListener = new TestKeyguardChangeListener();
mConfigChangeListener = new TestConfigurationChangeListener();
+ mUserChangeListener = new TestUserChangeListener();
mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
mController.onConfigurationChanged(getConfigurationCopy());
}
@@ -68,6 +81,46 @@ public class ShellControllerTest extends ShellTestCase {
}
@Test
+ public void testAddUserChangeListener_ensureCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 1);
+ assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+ }
+
+ @Test
+ public void testDoubleAddUserChangeListener_ensureSingleCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+ mController.addUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 1);
+ assertTrue(mUserChangeListener.lastUserContext == mTestUserContext);
+ }
+
+ @Test
+ public void testAddRemoveUserChangeListener_ensureNoCallback() {
+ mController.addUserChangeListener(mUserChangeListener);
+ mController.removeUserChangeListener(mUserChangeListener);
+
+ mController.onUserChanged(TEST_USER_ID, mTestUserContext);
+ assertTrue(mUserChangeListener.userChanged == 0);
+ assertTrue(mUserChangeListener.lastUserContext == null);
+ }
+
+ @Test
+ public void testUserProfilesChanged() {
+ mController.addUserChangeListener(mUserChangeListener);
+
+ ArrayList<UserInfo> profiles = new ArrayList<>();
+ profiles.add(mock(UserInfo.class));
+ profiles.add(mock(UserInfo.class));
+ mController.onUserProfilesChanged(profiles);
+ assertTrue(mUserChangeListener.lastUserProfiles.equals(profiles));
+ }
+
+ @Test
public void testAddKeyguardChangeListener_ensureCallback() {
mController.addKeyguardChangeListener(mKeyguardChangeListener);
@@ -332,4 +385,27 @@ public class ShellControllerTest extends ShellTestCase {
dismissAnimationFinished++;
}
}
+
+ private class TestUserChangeListener implements UserChangeListener {
+ // Counts of number of times each of the callbacks are called
+ public int userChanged;
+ public int lastUserId;
+ public Context lastUserContext;
+ public int userProfilesChanged;
+ public List<? extends UserInfo> lastUserProfiles;
+
+
+ @Override
+ public void onUserChanged(int newUserId, @NonNull Context userContext) {
+ userChanged++;
+ lastUserId = newUserId;
+ lastUserContext = userContext;
+ }
+
+ @Override
+ public void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {
+ userProfilesChanged++;
+ lastUserProfiles = profiles;
+ }
+ }
}