summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java142
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java44
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java20
-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.java9
-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.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java4
-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/phone/PipMotionHelper.java4
-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/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/DefaultTransitionHandler.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt143
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt14
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt19
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt177
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt4
-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/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
56 files changed, 2089 insertions, 427 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 6bfb16a3c22d..f24401f0cd53 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -20,21 +20,26 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+import static androidx.window.util.ExtensionHelper.isZero;
import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.Application;
import android.app.WindowConfiguration;
+import android.content.ComponentCallbacks;
import android.content.Context;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
import android.util.ArrayMap;
+import android.window.WindowContext;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiContext;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
@@ -58,11 +63,14 @@ import java.util.function.Consumer;
public class WindowLayoutComponentImpl implements WindowLayoutComponent {
private static final String TAG = "SampleExtension";
- private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
+ private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
new ArrayMap<>();
private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer;
+ private final Map<IBinder, WindowContextConfigListener> mWindowContextConfigListeners =
+ new ArrayMap<>();
+
public WindowLayoutComponentImpl(@NonNull Context context) {
((Application) context.getApplicationContext())
.registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged());
@@ -78,14 +86,42 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
* @param activity hosting a {@link android.view.Window}
* @param consumer interested in receiving updates to {@link WindowLayoutInfo}
*/
+ @Override
public void addWindowLayoutInfoListener(@NonNull Activity activity,
@NonNull Consumer<WindowLayoutInfo> consumer) {
+ addWindowLayoutInfoListener((Context) activity, consumer);
+ }
+
+ /**
+ * Similar to {@link #addWindowLayoutInfoListener(Activity, Consumer)}, but takes a UI Context
+ * as a parameter.
+ */
+ // TODO(b/204073440): Add @Override to hook the API in WM extensions library.
+ public void addWindowLayoutInfoListener(@NonNull @UiContext Context context,
+ @NonNull Consumer<WindowLayoutInfo> consumer) {
+ if (mWindowLayoutChangeListeners.containsKey(context)
+ || mWindowLayoutChangeListeners.containsValue(consumer)) {
+ // Early return if the listener or consumer has been registered.
+ return;
+ }
+ if (!context.isUiContext()) {
+ throw new IllegalArgumentException("Context must be a UI Context, which should be"
+ + " an Activity or a WindowContext");
+ }
mFoldingFeatureProducer.getData((features) -> {
// Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
- WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, features);
+ WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
consumer.accept(newWindowLayout);
});
- mWindowLayoutChangeListeners.put(activity, consumer);
+ mWindowLayoutChangeListeners.put(context, consumer);
+
+ if (context instanceof WindowContext) {
+ final IBinder windowContextToken = context.getWindowContextToken();
+ final WindowContextConfigListener listener =
+ new WindowContextConfigListener(windowContextToken);
+ context.registerComponentCallbacks(listener);
+ mWindowContextConfigListeners.put(windowContextToken, listener);
+ }
}
/**
@@ -93,18 +129,30 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
*
* @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo}
*/
+ @Override
public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) {
+ for (Context context : mWindowLayoutChangeListeners.keySet()) {
+ if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) {
+ continue;
+ }
+ if (context instanceof WindowContext) {
+ final IBinder token = context.getWindowContextToken();
+ context.unregisterComponentCallbacks(mWindowContextConfigListeners.get(token));
+ mWindowContextConfigListeners.remove(token);
+ }
+ break;
+ }
mWindowLayoutChangeListeners.values().remove(consumer);
}
@NonNull
- Set<Activity> getActivitiesListeningForLayoutChanges() {
+ Set<Context> getContextsListeningForLayoutChanges() {
return mWindowLayoutChangeListeners.keySet();
}
private boolean isListeningForLayoutChanges(IBinder token) {
- for (Activity activity: getActivitiesListeningForLayoutChanges()) {
- if (token.equals(activity.getWindow().getAttributes().token)) {
+ for (Context context: getContextsListeningForLayoutChanges()) {
+ if (token.equals(Context.getToken(context))) {
return true;
}
}
@@ -138,10 +186,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
}
private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
- for (Activity activity : getActivitiesListeningForLayoutChanges()) {
+ for (Context context : getContextsListeningForLayoutChanges()) {
// Get the WindowLayoutInfo from the activity and pass the value to the layoutConsumer.
- Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(activity);
- WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(activity, storedFeatures);
+ Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get(context);
+ WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures);
layoutConsumer.accept(newWindowLayout);
}
}
@@ -149,11 +197,12 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
/**
* Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a
* valid state is found.
- * @param activity a proxy for the {@link android.view.Window} that contains the
+ * @param context a proxy for the {@link android.view.Window} that contains the
+ * {@link DisplayFeature}.
*/
- private WindowLayoutInfo getWindowLayoutInfo(
- @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) {
- List<DisplayFeature> displayFeatureList = getDisplayFeatures(activity, storedFeatures);
+ private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context,
+ List<CommonFoldingFeature> storedFeatures) {
+ List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures);
return new WindowLayoutInfo(displayFeatureList);
}
@@ -170,18 +219,18 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
* bounds are not valid, constructing a {@link FoldingFeature} will throw an
* {@link IllegalArgumentException} since this can cause negative UI effects down stream.
*
- * @param activity a proxy for the {@link android.view.Window} that contains the
+ * @param context a proxy for the {@link android.view.Window} that contains the
* {@link DisplayFeature}.
* are within the {@link android.view.Window} of the {@link Activity}
*/
private List<DisplayFeature> getDisplayFeatures(
- @NonNull Activity activity, List<CommonFoldingFeature> storedFeatures) {
+ @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) {
List<DisplayFeature> features = new ArrayList<>();
- if (!shouldReportDisplayFeatures(activity)) {
+ if (!shouldReportDisplayFeatures(context)) {
return features;
}
- int displayId = activity.getDisplay().getDisplayId();
+ int displayId = context.getDisplay().getDisplayId();
for (CommonFoldingFeature baseFeature : storedFeatures) {
Integer state = convertToExtensionState(baseFeature.getState());
if (state == null) {
@@ -189,9 +238,9 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
}
Rect featureRect = baseFeature.getRect();
rotateRectToDisplayRotation(displayId, featureRect);
- transformToWindowSpaceRect(activity, featureRect);
+ transformToWindowSpaceRect(context, featureRect);
- if (!isRectZero(featureRect)) {
+ if (!isZero(featureRect)) {
// TODO(b/228641877): Remove guarding when fixed.
features.add(new FoldingFeature(featureRect, baseFeature.getType(), state));
}
@@ -203,15 +252,21 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
* Checks whether display features should be reported for the activity.
* TODO(b/238948678): Support reporting display features in all windowing modes.
*/
- private boolean shouldReportDisplayFeatures(@NonNull Activity activity) {
- int displayId = activity.getDisplay().getDisplayId();
+ private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) {
+ int displayId = context.getDisplay().getDisplayId();
if (displayId != DEFAULT_DISPLAY) {
// Display features are not supported on secondary displays.
return false;
}
- final int taskWindowingMode = ActivityClient.getInstance().getTaskWindowingMode(
- activity.getActivityToken());
- if (taskWindowingMode == -1) {
+ final int windowingMode;
+ if (context instanceof Activity) {
+ windowingMode = ActivityClient.getInstance().getTaskWindowingMode(
+ context.getActivityToken());
+ } else {
+ windowingMode = context.getResources().getConfiguration().windowConfiguration
+ .getWindowingMode();
+ }
+ if (windowingMode == -1) {
// If we cannot determine the task windowing mode for any reason, it is likely that we
// won't be able to determine its position correctly as well. DisplayFeatures' bounds
// in this case can't be computed correctly, so we should skip.
@@ -219,36 +274,43 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
}
// It is recommended not to report any display features in multi-window mode, since it
// won't be possible to synchronize the display feature positions with window movement.
- return !WindowConfiguration.inMultiWindowMode(taskWindowingMode);
+ return !WindowConfiguration.inMultiWindowMode(windowingMode);
}
- /**
- * Returns {@link true} if a {@link Rect} has zero width and zero height,
- * {@code false} otherwise.
- */
- private boolean isRectZero(Rect rect) {
- return rect.width() == 0 && rect.height() == 0;
+ private void onDisplayFeaturesChangedIfListening(@NonNull IBinder token) {
+ if (isListeningForLayoutChanges(token)) {
+ mFoldingFeatureProducer.getData(
+ WindowLayoutComponentImpl.this::onDisplayFeaturesChanged);
+ }
}
private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
super.onActivityCreated(activity, savedInstanceState);
- onDisplayFeaturesChangedIfListening(activity);
+ onDisplayFeaturesChangedIfListening(activity.getActivityToken());
}
@Override
public void onActivityConfigurationChanged(Activity activity) {
super.onActivityConfigurationChanged(activity);
- onDisplayFeaturesChangedIfListening(activity);
+ onDisplayFeaturesChangedIfListening(activity.getActivityToken());
+ }
+ }
+
+ private final class WindowContextConfigListener implements ComponentCallbacks {
+ final IBinder mToken;
+
+ WindowContextConfigListener(IBinder token) {
+ mToken = token;
}
- private void onDisplayFeaturesChangedIfListening(Activity activity) {
- IBinder token = activity.getWindow().getAttributes().token;
- if (token == null || isListeningForLayoutChanges(token)) {
- mFoldingFeatureProducer.getData(
- WindowLayoutComponentImpl.this::onDisplayFeaturesChanged);
- }
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ onDisplayFeaturesChangedIfListening(mToken);
}
+
+ @Override
+ public void onLowMemory() {}
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
index 0da44ac36a6e..cbaa27712015 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
@@ -16,6 +16,7 @@
package androidx.window.util;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import java.util.LinkedHashSet;
@@ -25,25 +26,45 @@ import java.util.function.Consumer;
/**
* Base class that provides the implementation for the callback mechanism of the
- * {@link DataProducer} API.
+ * {@link DataProducer} API. This class is thread safe for adding, removing, and notifying
+ * consumers.
*
* @param <T> The type of data this producer returns through {@link DataProducer#getData}.
*/
public abstract class BaseDataProducer<T> implements DataProducer<T> {
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
private final Set<Consumer<T>> mCallbacks = new LinkedHashSet<>();
+ /**
+ * Adds a callback to the set of callbacks listening for data. Data is delivered through
+ * {@link BaseDataProducer#notifyDataChanged(Object)}. This method is thread safe. Callers
+ * should ensure that callbacks are thread safe.
+ * @param callback that will receive data from the producer.
+ */
@Override
public final void addDataChangedCallback(@NonNull Consumer<T> callback) {
- mCallbacks.add(callback);
- Optional<T> currentData = getCurrentData();
- currentData.ifPresent(callback);
- onListenersChanged(mCallbacks);
+ synchronized (mLock) {
+ mCallbacks.add(callback);
+ Optional<T> currentData = getCurrentData();
+ currentData.ifPresent(callback);
+ onListenersChanged(mCallbacks);
+ }
}
+ /**
+ * Removes a callback to the set of callbacks listening for data. This method is thread safe
+ * for adding.
+ * @param callback that was registered in
+ * {@link BaseDataProducer#addDataChangedCallback(Consumer)}.
+ */
@Override
public final void removeDataChangedCallback(@NonNull Consumer<T> callback) {
- mCallbacks.remove(callback);
- onListenersChanged(mCallbacks);
+ synchronized (mLock) {
+ mCallbacks.remove(callback);
+ onListenersChanged(mCallbacks);
+ }
}
protected void onListenersChanged(Set<Consumer<T>> callbacks) {}
@@ -56,11 +77,14 @@ public abstract class BaseDataProducer<T> implements DataProducer<T> {
/**
* Called to notify all registered consumers that the data provided
- * by {@link DataProducer#getData} has changed.
+ * by {@link DataProducer#getData} has changed. Calls to this are thread save but callbacks need
+ * to ensure thread safety.
*/
protected void notifyDataChanged(T value) {
- for (Consumer<T> callback : mCallbacks) {
- callback.accept(value);
+ synchronized (mLock) {
+ for (Consumer<T> callback : mCallbacks) {
+ callback.accept(value);
+ }
}
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
index 2a593f15a9de..31bf96313a95 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/ExtensionHelper.java
@@ -21,14 +21,15 @@ import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
-import android.app.Activity;
+import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.view.DisplayInfo;
import android.view.Surface;
+import android.view.WindowManager;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
+import androidx.annotation.UiContext;
/**
* Util class for both Sidecar and Extensions.
@@ -86,12 +87,9 @@ public final class ExtensionHelper {
}
/** Transforms rectangle from absolute coordinate space to the window coordinate space. */
- public static void transformToWindowSpaceRect(Activity activity, Rect inOutRect) {
- Rect windowRect = getWindowBounds(activity);
- if (windowRect == null) {
- inOutRect.setEmpty();
- return;
- }
+ public static void transformToWindowSpaceRect(@NonNull @UiContext Context context,
+ Rect inOutRect) {
+ Rect windowRect = getWindowBounds(context);
if (!Rect.intersects(inOutRect, windowRect)) {
inOutRect.setEmpty();
return;
@@ -103,9 +101,9 @@ public final class ExtensionHelper {
/**
* Gets the current window bounds in absolute coordinates.
*/
- @Nullable
- private static Rect getWindowBounds(@NonNull Activity activity) {
- return activity.getWindowManager().getCurrentWindowMetrics().getBounds();
+ @NonNull
+ private static Rect getWindowBounds(@NonNull @UiContext Context context) {
+ return context.getSystemService(WindowManager.class).getCurrentWindowMetrics().getBounds();
}
/**
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 2666a0e186b3..cfbe1b3caf7a 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..aeaf6eda9809 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
@@ -111,6 +111,9 @@ public class BubbleStackView extends FrameLayout
public static final boolean HOME_GESTURE_ENABLED =
SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", true);
+ public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE =
+ SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true);
+
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
/** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
@@ -299,7 +302,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 +316,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..b91062f891e8 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.animation;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE;
import static com.android.wm.shell.bubbles.BubbleStackView.HOME_GESTURE_ENABLED;
import android.content.res.Resources;
@@ -366,6 +367,7 @@ public class ExpandedAnimationController
mMagnetizedBubbleDraggingOut.setMagnetListener(listener);
mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ mMagnetizedBubbleDraggingOut.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
}
private void springBubbleTo(View bubble, float x, float y) {
@@ -468,7 +470,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..961722ba9bc0 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.bubbles.animation;
import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING;
+import static com.android.wm.shell.bubbles.BubbleStackView.ENABLE_FLING_TO_DISMISS_BUBBLE;
import android.content.ContentResolver;
import android.content.res.Resources;
@@ -431,7 +432,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: ");
@@ -1028,6 +1029,7 @@ public class StackAnimationController extends
};
mMagnetizedStack.setHapticsEnabled(true);
mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+ mMagnetizedStack.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE);
}
final ContentResolver contentResolver = mLayout.getContext().getContentResolver();
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/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 44d22029a5e9..afb64c9eec41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -33,6 +33,7 @@ import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Debug;
+import android.os.SystemProperties;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
@@ -58,6 +59,8 @@ import kotlin.jvm.functions.Function0;
public class PipMotionHelper implements PipAppOpsListener.Callback,
FloatingContentCoordinator.FloatingContent {
+ public static final boolean ENABLE_FLING_TO_DISMISS_PIP =
+ SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true);
private static final String TAG = "PipMotionHelper";
private static final boolean DEBUG = false;
@@ -704,6 +707,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
loc[1] = animatedPipBounds.top;
}
};
+ mMagnetizedPip.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_PIP);
}
return mMagnetizedPip;
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/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/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 6c659667a4a7..cff60f5e5b6c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -44,6 +44,8 @@ import static android.view.WindowManager.TRANSIT_RELAUNCH;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.transitTypeToString;
+import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
+import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION;
@@ -903,11 +905,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private void attachThumbnail(@NonNull ArrayList<Animator> animations,
@NonNull Runnable finishCallback, TransitionInfo.Change change,
TransitionInfo.AnimationOptions options, float cornerRadius) {
- final boolean isTask = change.getTaskInfo() != null;
final boolean isOpen = Transitions.isOpeningType(change.getMode());
final boolean isClose = Transitions.isClosingType(change.getMode());
if (isOpen) {
- if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) {
+ if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) {
attachCrossProfileThumbnailAnimation(animations, finishCallback, change,
cornerRadius);
} else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) {
@@ -922,8 +923,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@NonNull Runnable finishCallback, TransitionInfo.Change change, float cornerRadius) {
final Rect bounds = change.getEndAbsBounds();
// Show the right drawable depending on the user we're transitioning to.
- final Drawable thumbnailDrawable = change.getTaskInfo().userId == mCurrentUserId
- ? mContext.getDrawable(R.drawable.ic_account_circle) : mEnterpriseThumbnailDrawable;
+ final Drawable thumbnailDrawable = change.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL)
+ ? mContext.getDrawable(R.drawable.ic_account_circle)
+ : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL)
+ ? mEnterpriseThumbnailDrawable : null;
+ if (thumbnailDrawable == null) {
+ return;
+ }
final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail(
thumbnailDrawable, bounds);
if (thumbnail == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
index bdcdb63d2cd6..cc4d268a0000 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
@@ -34,4 +34,9 @@ interface IShellTransitions {
* Unregisters a remote transition handler.
*/
oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2;
+
+ /**
+ * Retrieves the apply-token used by transactions in Shell
+ */
+ IBinder getShellApplyToken() = 3;
}
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..d2e8624171f6 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());
}
@@ -956,6 +958,11 @@ public class Transitions implements RemoteCallable<Transitions> {
transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition);
});
}
+
+ @Override
+ public IBinder getShellApplyToken() {
+ return SurfaceControl.Transaction.getDefaultApplyToken();
+ }
}
private class SettingsObserver extends ContentObserver {
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/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index 330c9c95e484..cb74315732ab 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -20,6 +20,7 @@ package com.android.wm.shell.flicker
import android.view.Surface
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject
import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import com.android.server.wm.traces.common.IComponentMatcher
import com.android.server.wm.traces.common.region.Region
@@ -94,25 +95,27 @@ fun FlickerTestParameter.layerKeepVisible(
fun FlickerTestParameter.splitAppLayerBoundsBecomesVisible(
component: IComponentMatcher,
- splitLeftTop: Boolean
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayers {
- this.isInvisible(component)
+ this.notContains(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component), isOptional = true)
.then()
- .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
- .isVisible(component)
+ .isInvisible(SPLIT_SCREEN_DIVIDER_COMPONENT.or(component))
.then()
- .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
- .splitAppLayerBoundsSnapToDivider(component, splitLeftTop, endRotation)
+ .splitAppLayerBoundsSnapToDivider(
+ component, landscapePosLeft, portraitPosTop, endRotation)
}
}
fun FlickerTestParameter.splitAppLayerBoundsBecomesInvisible(
component: IComponentMatcher,
- splitLeftTop: Boolean
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayers {
- this.splitAppLayerBoundsSnapToDivider(component, splitLeftTop, endRotation)
+ this.splitAppLayerBoundsSnapToDivider(
+ component, landscapePosLeft, portraitPosTop, endRotation)
.then()
.isVisible(component, true)
.then()
@@ -122,58 +125,96 @@ fun FlickerTestParameter.splitAppLayerBoundsBecomesInvisible(
fun FlickerTestParameter.splitAppLayerBoundsIsVisibleAtEnd(
component: IComponentMatcher,
- splitLeftTop: Boolean
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayersEnd {
- val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
- visibleRegion(component).coversAtMost(
- if (splitLeftTop) {
- getSplitLeftTopRegion(dividerRegion, endRotation)
- } else {
- getSplitRightBottomRegion(dividerRegion, endRotation)
- }
- )
+ splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, endRotation)
}
}
fun FlickerTestParameter.splitAppLayerBoundsKeepVisible(
component: IComponentMatcher,
- splitLeftTop: Boolean
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayers {
- this.splitAppLayerBoundsSnapToDivider(component, splitLeftTop, endRotation)
+ splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, endRotation)
}
}
fun FlickerTestParameter.splitAppLayerBoundsChanges(
component: IComponentMatcher,
- splitLeftTop: Boolean
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean
) {
assertLayers {
- if (splitLeftTop) {
- this.splitAppLayerBoundsSnapToDivider(component, splitLeftTop, endRotation)
+ if (landscapePosLeft) {
+ this.splitAppLayerBoundsSnapToDivider(
+ component, landscapePosLeft, portraitPosTop, endRotation)
.then()
.isInvisible(component)
.then()
- .splitAppLayerBoundsSnapToDivider(component, splitLeftTop, endRotation)
+ .splitAppLayerBoundsSnapToDivider(
+ component, landscapePosLeft, portraitPosTop, endRotation)
} else {
- this.splitAppLayerBoundsSnapToDivider(component, splitLeftTop, endRotation)
+ this.splitAppLayerBoundsSnapToDivider(
+ component, landscapePosLeft, portraitPosTop, endRotation)
}
}
}
fun LayersTraceSubject.splitAppLayerBoundsSnapToDivider(
component: IComponentMatcher,
- splitLeftTop: Boolean,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean,
rotation: Int
): LayersTraceSubject {
return invoke("splitAppLayerBoundsSnapToDivider") {
- val dividerRegion = it.layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
- it.visibleRegion(component).coversAtMost(
- if (splitLeftTop) {
- getSplitLeftTopRegion(dividerRegion, rotation)
+ it.splitAppLayerBoundsSnapToDivider(component, landscapePosLeft, portraitPosTop, rotation)
+ }
+}
+
+fun LayerTraceEntrySubject.splitAppLayerBoundsSnapToDivider(
+ component: IComponentMatcher,
+ landscapePosLeft: Boolean,
+ portraitPosTop: Boolean,
+ rotation: Int
+): LayerTraceEntrySubject {
+ val displayBounds = WindowUtils.getDisplayBounds(rotation)
+ return invoke {
+ val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
+ visibleRegion(component).coversAtMost(
+ if (displayBounds.width > displayBounds.height) {
+ if (landscapePosLeft) {
+ Region.from(
+ 0,
+ 0,
+ (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
+ displayBounds.bounds.bottom)
+ } else {
+ Region.from(
+ (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
+ 0,
+ displayBounds.bounds.right,
+ displayBounds.bounds.bottom
+ )
+ }
} else {
- getSplitRightBottomRegion(dividerRegion, rotation)
+ if (portraitPosTop) {
+ Region.from(
+ 0,
+ 0,
+ displayBounds.bounds.right,
+ (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2)
+ } else {
+ Region.from(
+ 0,
+ (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2,
+ displayBounds.bounds.right,
+ displayBounds.bounds.bottom
+ )
+ }
}
)
}
@@ -185,6 +226,10 @@ fun FlickerTestParameter.appWindowBecomesVisible(
assertWm {
this.isAppWindowInvisible(component)
.then()
+ .notContains(component, isOptional = true)
+ .then()
+ .isAppWindowInvisible(component, isOptional = true)
+ .then()
.isAppWindowVisible(component)
}
}
@@ -208,7 +253,7 @@ fun FlickerTestParameter.appWindowIsVisibleAtEnd(
}
fun FlickerTestParameter.appWindowKeepVisible(
- component: IComponentMatcher
+ component: IComponentMatcher
) {
assertWm {
this.isAppWindowVisible(component)
@@ -316,39 +361,3 @@ fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region {
)
}
}
-
-fun getSplitLeftTopRegion(dividerRegion: Region, rotation: Int): Region {
- val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return if (displayBounds.width > displayBounds.height) {
- Region.from(
- 0,
- 0,
- (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
- displayBounds.bounds.bottom)
- } else {
- Region.from(
- 0,
- 0,
- displayBounds.bounds.right,
- (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2)
- }
-}
-
-fun getSplitRightBottomRegion(dividerRegion: Region, rotation: Int): Region {
- val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return if (displayBounds.width > displayBounds.height) {
- Region.from(
- (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
- 0,
- displayBounds.bounds.right,
- displayBounds.bounds.bottom
- )
- } else {
- Region.from(
- 0,
- (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2,
- displayBounds.bounds.right,
- displayBounds.bounds.bottom
- )
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
index 1950e486f34b..3ad92f87421b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
@@ -21,6 +21,7 @@ import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import androidx.test.filters.RequiresDevice
import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.FlickerTestParameter
@@ -58,26 +59,27 @@ open class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen
setup {
test {
for (i in 1..3) {
- val addBubbleBtn = waitAndGetAddBubbleBtn()
- addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found")
+ val addBubbleBtn = waitAndGetAddBubbleBtn() ?: error("Add Bubble not found")
+ addBubbleBtn.click()
+ SystemClock.sleep(1000)
}
val showBubble = device.wait(
Until.findObject(
By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)
), FIND_OBJECT_TIMEOUT
- )
- showBubble?.run { showBubble.click() } ?: error("Show bubble not found")
+ ) ?: error("Show bubble not found")
+ showBubble.click()
SystemClock.sleep(1000)
}
}
transitions {
- val bubbles = device.wait(
+ val bubbles: List<UiObject2> = device.wait(
Until.findObjects(
By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)
), FIND_OBJECT_TIMEOUT
) ?: error("No bubbles found")
for (entry in bubbles) {
- entry?.run { entry.click() } ?: error("Bubble not found")
+ entry.click()
SystemClock.sleep(1000)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
index 240e8711d43e..e7f9d9a9d73d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
@@ -105,6 +105,25 @@ class SplitScreenHelper(
.waitForAndVerify()
}
+ fun splitFromOverview(tapl: LauncherInstrumentation) {
+ // Note: The initial split position in landscape is different between tablet and phone.
+ // In landscape, tablet will let the first app split to right side, and phone will
+ // split to left side.
+ if (tapl.isTablet) {
+ tapl.workspace.switchToOverview().overviewActions
+ .clickSplit()
+ .currentTask
+ .open()
+ } else {
+ tapl.workspace.switchToOverview().currentTask
+ .tapMenu()
+ .tapSplitMenuItem()
+ .currentTask
+ .open()
+ }
+ SystemClock.sleep(TIMEOUT_MS)
+ }
+
fun dragFromNotificationToSplit(
instrumentation: Instrumentation,
device: UiDevice,
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index f69107eae638..d23881475ad6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -92,12 +92,12 @@ class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testS
@Presubmit
@Test
fun primaryAppBoundsKeepVisible() = testSpec.splitAppLayerBoundsKeepVisible(
- primaryApp, splitLeftTop = true)
+ primaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
fun textEditAppBoundsKeepVisible() = testSpec.splitAppLayerBoundsKeepVisible(
- textEditApp, splitLeftTop = false)
+ textEditApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index cd92db74af95..ba40c2740bb1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -98,7 +98,7 @@ class DismissSplitScreenByDivider (testSpec: FlickerTestParameter) : SplitScreen
@Presubmit
@Test
fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index 127ac1e7162b..6828589656d6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -96,12 +96,12 @@ class DismissSplitScreenByGoHome(
@Presubmit
@Test
fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 0f4d98d69c00..9ac7c230096b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -108,12 +108,12 @@ class DragDividerToResize (testSpec: FlickerTestParameter) : SplitScreenBase(tes
@Presubmit
@Test
fun primaryAppBoundsChanges() = testSpec.splitAppLayerBoundsChanges(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsChanges() = testSpec.splitAppLayerBoundsChanges(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
/** {@inheritDoc} */
@Postsubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
index 9564d975194b..8401c1a910b8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -94,12 +94,12 @@ class EnterSplitScreenByDragFromAllApps(
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
index 3b59716180b6..168afda119a9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -109,12 +109,12 @@ class EnterSplitScreenByDragFromNotification(
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
- sendNotificationApp, splitLeftTop = true)
+ sendNotificationApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
index 3de98723e132..c1fce5f40b57 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -97,12 +97,12 @@ class EnterSplitScreenByDragFromTaskbar(
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
new file mode 100644
index 000000000000..8cb5d7c24ced
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.flicker.splitscreen
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowBecomesVisible
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen from Overview.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenFromOverview`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) {
+
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ super.transition(this)
+ setup {
+ eachRun {
+ tapl.workspace.switchToOverview().dismissAllTasks()
+ primaryApp.launchViaIntent(wmHelper)
+ secondaryApp.launchViaIntent(wmHelper)
+ tapl.goHome()
+ wmHelper.StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withHomeActivityVisible()
+ .waitForAndVerify()
+ }
+ }
+ transitions {
+ SplitScreenHelper.splitFromOverview(tapl)
+ SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+ @Presubmit
+ @Test
+ fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp)
+
+ @Presubmit
+ @Test
+ fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+ primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
+
+ @Presubmit
+ @Test
+ fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
+ secondaryApp, landscapePosLeft = !tapl.isTablet, portraitPosTop = true)
+
+ @Presubmit
+ @Test
+ fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp)
+
+ @Presubmit
+ @Test
+ fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp)
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun entireScreenCovered() =
+ super.entireScreenCovered()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerIsVisibleAtStartAndEnd() =
+ super.navBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarLayerPositionAtStartAndEnd() =
+ super.navBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun navBarWindowIsAlwaysVisible() =
+ super.navBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() =
+ super.statusBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() =
+ super.statusBarLayerPositionAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() =
+ super.statusBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() =
+ super.taskBarLayerIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() =
+ super.taskBarWindowIsAlwaysVisible()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Postsubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
+ super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): List<FlickerTestParameter> {
+ return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+ repetitions = SplitScreenHelper.TEST_REPETITIONS)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index bdfd9c7de32f..153056188d24 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -94,12 +94,12 @@ class SwitchAppByDoubleTapDivider (testSpec: FlickerTestParameter) : SplitScreen
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = true)
+ primaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- secondaryApp, splitLeftTop = false)
+ secondaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index da954d97aec2..20544bd2fc2f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -98,12 +98,12 @@ class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScr
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index db89ff52178b..5a8604f2dccc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -97,12 +97,12 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index c23cdb610671..adea66a49c46 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -99,12 +99,12 @@ class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenB
@Presubmit
@Test
fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- primaryApp, splitLeftTop = false)
+ primaryApp, landscapePosLeft = false, portraitPosTop = false)
@Presubmit
@Test
fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
- secondaryApp, splitLeftTop = true)
+ secondaryApp, landscapePosLeft = true, portraitPosTop = true)
@Presubmit
@Test
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/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;
+ }
+ }
}