diff options
Diffstat (limited to 'libs')
33 files changed, 747 insertions, 420 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java deleted file mode 100644 index ff49cdcab349..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/OverlayCreateParams.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.window.extensions.embedding; - -import static java.util.Objects.requireNonNull; - -import android.graphics.Rect; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -/** - * The parameter to create an overlay container that retrieved from - * {@link android.app.ActivityOptions} bundle. - */ -class OverlayCreateParams { - - // TODO(b/295803704): Move them to WM Extensions so that we can reuse in WM Jetpack. - @VisibleForTesting - static final String KEY_OVERLAY_CREATE_PARAMS = - "androidx.window.extensions.OverlayCreateParams"; - - @VisibleForTesting - static final String KEY_OVERLAY_CREATE_PARAMS_TASK_ID = - "androidx.window.extensions.OverlayCreateParams.taskId"; - - @VisibleForTesting - static final String KEY_OVERLAY_CREATE_PARAMS_TAG = - "androidx.window.extensions.OverlayCreateParams.tag"; - - @VisibleForTesting - static final String KEY_OVERLAY_CREATE_PARAMS_BOUNDS = - "androidx.window.extensions.OverlayCreateParams.bounds"; - - private final int mTaskId; - - @NonNull - private final String mTag; - - @NonNull - private final Rect mBounds; - - OverlayCreateParams(int taskId, @NonNull String tag, @NonNull Rect bounds) { - mTaskId = taskId; - mTag = requireNonNull(tag); - mBounds = requireNonNull(bounds); - } - - int getTaskId() { - return mTaskId; - } - - @NonNull - String getTag() { - return mTag; - } - - @NonNull - Rect getBounds() { - return mBounds; - } - - @Override - public int hashCode() { - int result = mTaskId; - result = 31 * result + mTag.hashCode(); - result = 31 * result + mBounds.hashCode(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (!(obj instanceof OverlayCreateParams thatParams)) return false; - return mTaskId == thatParams.mTaskId - && mTag.equals(thatParams.mTag) - && mBounds.equals(thatParams.mBounds); - } - - @Override - public String toString() { - return OverlayCreateParams.class.getSimpleName() + ": {" - + "taskId=" + mTaskId - + ", tag=" + mTag - + ", bounds=" + mBounds - + "}"; - } - - /** Retrieves the {@link OverlayCreateParams} from {@link android.app.ActivityOptions} bundle */ - @Nullable - static OverlayCreateParams fromBundle(@NonNull Bundle bundle) { - final Bundle paramsBundle = bundle.getBundle(KEY_OVERLAY_CREATE_PARAMS); - if (paramsBundle == null) { - return null; - } - final int taskId = paramsBundle.getInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID); - final String tag = requireNonNull(paramsBundle.getString(KEY_OVERLAY_CREATE_PARAMS_TAG)); - final Rect bounds = requireNonNull(paramsBundle.getParcelable( - KEY_OVERLAY_CREATE_PARAMS_BOUNDS, Rect.class)); - - return new OverlayCreateParams(taskId, tag, bounds); - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 4973a4d85af7..15ee4e1d4adf 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -34,6 +34,7 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHA import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; +import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; @@ -136,6 +137,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private Function<SplitAttributesCalculatorParams, SplitAttributes> mSplitAttributesCalculator; /** + * A calculator function to compute {@link ActivityStack} attributes in a task, which is called + * when there's {@link #onTaskFragmentParentInfoChanged} or folding state changed. + */ + @GuardedBy("mLock") + @Nullable + private Function<ActivityStackAttributesCalculatorParams, ActivityStackAttributes> + mActivityStackAttributesCalculator; + + /** * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info * below it. * When the app is host of multiple Tasks, there can be multiple splits controlled by the same @@ -319,6 +329,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } + @Override + public void setActivityStackAttributesCalculator( + @NonNull Function<ActivityStackAttributesCalculatorParams, ActivityStackAttributes> + calculator) { + synchronized (mLock) { + mActivityStackAttributesCalculator = calculator; + } + } + + @Override + public void clearActivityStackAttributesCalculator() { + synchronized (mLock) { + mActivityStackAttributesCalculator = null; + } + } + @GuardedBy("mLock") @Nullable Function<SplitAttributesCalculatorParams, SplitAttributes> getSplitAttributesCalculator() { @@ -1412,7 +1438,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @Nullable Activity launchingActivity) { return createEmptyContainer(wct, intent, taskId, new Rect(), launchingActivity, - null /* overlayTag */); + null /* overlayTag */, null /* launchOptions */); } /** @@ -1426,7 +1452,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer createEmptyContainer( @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @NonNull Rect bounds, @Nullable Activity launchingActivity, - @Nullable String overlayTag) { + @Nullable String overlayTag, @Nullable Bundle launchOptions) { // We need an activity in the organizer process in the same Task to use as the owner // activity, as well as to get the Task window info. final Activity activityInTask; @@ -1443,7 +1469,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return null; } final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */, - intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag); + intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag, + launchOptions); final IBinder taskFragmentToken = container.getTaskFragmentToken(); // Note that taskContainer will not exist before calling #newContainer if the container // is the first embedded TF in the task. @@ -1570,14 +1597,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, @NonNull Activity activityInTask, int taskId) { return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */, - activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */); + activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */, + null /* launchOptions */); } @GuardedBy("mLock") TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */); + activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */, + null /* launchOptions */); } @GuardedBy("mLock") @@ -1585,7 +1614,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull Activity activityInTask, int taskId, @NonNull TaskFragmentContainer pairedPrimaryContainer) { return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent, - activityInTask, taskId, pairedPrimaryContainer, null /* tag */); + activityInTask, taskId, pairedPrimaryContainer, null /* tag */, + null /* launchOptions */); } /** @@ -1602,11 +1632,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * @param overlayTag The tag for the new created overlay container. It must be * needed if {@code isOverlay} is {@code true}. Otherwise, * it should be {@code null}. + * @param launchOptions The launch options bundle to create a container. Must be + * specified for overlay container. */ @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId, - @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) { + @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, + @Nullable Bundle launchOptions) { if (activityInTask == null) { throw new IllegalArgumentException("activityInTask must not be null,"); } @@ -1615,7 +1648,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, - pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag); + pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag, + launchOptions); return container; } @@ -2345,28 +2379,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") @Nullable TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded( - @NonNull WindowContainerTransaction wct, - @NonNull OverlayCreateParams overlayCreateParams, int launchTaskId, + @NonNull WindowContainerTransaction wct, @NonNull Bundle options, @NonNull Intent intent, @NonNull Activity launchActivity) { - final int taskId = overlayCreateParams.getTaskId(); - if (taskId != launchTaskId) { - // The task ID doesn't match the launch activity's. Cannot determine the host task - // to launch the overlay. - throw new IllegalArgumentException("The task ID of " - + "OverlayCreateParams#launchingActivity must match the task ID of " - + "the activity to #startActivity with the activity options that takes " - + "OverlayCreateParams."); - } final List<TaskFragmentContainer> overlayContainers = getAllOverlayTaskFragmentContainers(); - final String overlayTag = overlayCreateParams.getTag(); + final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); // If the requested bounds of OverlayCreateParams are smaller than minimum dimensions // specified by Intent, expand the overlay container to fill the parent task instead. - final Rect bounds = overlayCreateParams.getBounds(); - final Size minDimensions = getMinDimensions(intent); - final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(bounds, - minDimensions); + final ActivityStackAttributesCalculatorParams params = + new ActivityStackAttributesCalculatorParams(mPresenter.toParentContainerInfo( + mPresenter.getTaskProperties(launchActivity)), overlayTag, options); + // Fallback to expand the bounds if there's no activityStackAttributes calculator. + final Rect relativeBounds = mActivityStackAttributesCalculator != null + ? new Rect(mActivityStackAttributesCalculator.apply(params).getRelativeBounds()) + : new Rect(); + final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(relativeBounds, + getMinDimensions(intent)); + // Expand the bounds if the requested bounds are smaller than minimum dimensions. + if (shouldExpandContainer) { + relativeBounds.setEmpty(); + } + final int taskId = getTaskId(launchActivity); if (!overlayContainers.isEmpty()) { for (final TaskFragmentContainer overlayContainer : overlayContainers) { if (!overlayTag.equals(overlayContainer.getOverlayTag()) @@ -2390,7 +2424,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final Rect taskBounds = overlayContainer.getTaskContainer().getTaskProperties() .getTaskMetrics().getBounds(); final IBinder overlayToken = overlayContainer.getTaskFragmentToken(); - final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds); + final Rect sanitizedBounds = sanitizeBounds(relativeBounds, intent, taskBounds); mPresenter.resizeTaskFragment(wct, overlayToken, sanitizedBounds); mPresenter.setTaskFragmentIsolatedNavigation(wct, overlayToken, !sanitizedBounds.isEmpty()); @@ -2402,8 +2436,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } } - return createEmptyContainer(wct, intent, taskId, - (shouldExpandContainer ? new Rect() : bounds), launchActivity, overlayTag); + // Launch the overlay container to the task with taskId. + return createEmptyContainer(wct, intent, taskId, relativeBounds, launchActivity, overlayTag, + options); } private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @@ -2568,12 +2603,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer launchedInTaskFragment; if (launchingActivity != null) { final int taskId = getTaskId(launchingActivity); - final OverlayCreateParams overlayCreateParams = - OverlayCreateParams.fromBundle(options); + final String overlayTag = options.getString(KEY_OVERLAY_TAG); if (Flags.activityEmbeddingOverlayPresentationFlag() - && overlayCreateParams != null) { + && overlayTag != null) { launchedInTaskFragment = createOrUpdateOverlayTaskFragmentIfNeeded(wct, - overlayCreateParams, taskId, intent, launchingActivity); + options, intent, launchingActivity); } else { launchedInTaskFragment = resolveStartActivityIntent(wct, taskId, intent, launchingActivity); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index b5c32bbe78fa..acfd8e4314bf 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -1084,4 +1084,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) { return getTaskProperties(activity).getTaskMetrics(); } + + @NonNull + ParentContainerInfo toParentContainerInfo(@NonNull TaskProperties taskProperties) { + final Configuration configuration = taskProperties.getConfiguration(); + final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent + .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), + configuration.windowConfiguration); + return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration, + windowLayoutInfo); + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 3e7f99b96421..afd554b6e52b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -24,6 +24,7 @@ import android.app.WindowConfiguration.WindowingMode; import android.content.Intent; import android.graphics.Rect; import android.os.Binder; +import android.os.Bundle; import android.os.IBinder; import android.util.Size; import android.window.TaskFragmentAnimationParams; @@ -105,6 +106,13 @@ class TaskFragmentContainer { @Nullable private final String mOverlayTag; + /** + * The launch options that was used to create this container. Must not be {@code null} for + * {@link #isOverlay()} container. + */ + @Nullable + private final Bundle mLaunchOptions; + /** Indicates whether the container was cleaned up after the last activity was removed. */ private boolean mIsFinished; @@ -165,7 +173,7 @@ class TaskFragmentContainer { /** * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController, - * TaskFragmentContainer, String) + * TaskFragmentContainer, String, Bundle) */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @@ -173,7 +181,8 @@ class TaskFragmentContainer { @NonNull SplitController controller, @Nullable TaskFragmentContainer pairedPrimaryContainer) { this(pendingAppearedActivity, pendingAppearedIntent, taskContainer, - controller, pairedPrimaryContainer, null /* overlayTag */); + controller, pairedPrimaryContainer, null /* overlayTag */, + null /* launchOptions */); } /** @@ -181,11 +190,14 @@ class TaskFragmentContainer { * container transaction. * @param pairedPrimaryContainer when it is set, the new container will be add right above it * @param overlayTag Sets to indicate this taskFragment is an overlay container + * @param launchOptions The launch options to create this container. Must not be + * {@code null} for an overlay container */ TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, @NonNull SplitController controller, - @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag) { + @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, + @Nullable Bundle launchOptions) { if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( @@ -195,6 +207,10 @@ class TaskFragmentContainer { mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; mOverlayTag = overlayTag; + if (overlayTag != null) { + Objects.requireNonNull(launchOptions); + } + mLaunchOptions = launchOptions; if (pairedPrimaryContainer != null) { // The TaskFragment will be positioned right above the paired container. @@ -344,7 +360,7 @@ class TaskFragmentContainer { if (activities == null) { return null; } - return new ActivityStack(activities, isEmpty(), mToken); + return new ActivityStack(activities, isEmpty(), mToken, mOverlayTag); } /** Adds the activity that will be reparented to this container. */ @@ -901,8 +917,8 @@ class TaskFragmentContainer { } /** - * Returns the tag specified in {@link OverlayCreateParams#getTag()}. {@code null} if this - * taskFragment container is not an overlay container. + * Returns the tag specified in launch options. {@code null} if this taskFragment container is + * not an overlay container. */ @Nullable String getOverlayTag() { diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 4c2433fab2f8..678bdef3df92 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -18,14 +18,11 @@ package androidx.window.extensions.embedding; import static android.view.Display.DEFAULT_DISPLAY; +import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; -import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS; -import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_BOUNDS; -import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_TAG; -import static androidx.window.extensions.embedding.OverlayCreateParams.KEY_OVERLAY_CREATE_PARAMS_TASK_ID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -45,6 +42,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import android.app.Activity; +import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -98,9 +96,6 @@ public class OverlayPresentationTest { @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); - private static final OverlayCreateParams TEST_OVERLAY_CREATE_PARAMS = - new OverlayCreateParams(TASK_ID, "test,", new Rect(0, 0, 200, 200)); - private SplitController.ActivityStartMonitor mMonitor; private Intent mIntent; @@ -165,37 +160,15 @@ public class OverlayPresentationTest { } @Test - public void testOverlayCreateParamsFromBundle() { - assertThat(OverlayCreateParams.fromBundle(new Bundle())).isNull(); - - assertThat(OverlayCreateParams.fromBundle(createOverlayCreateParamsTestBundle())) - .isEqualTo(TEST_OVERLAY_CREATE_PARAMS); - } - - @Test public void testStartActivity_overlayFeatureDisabled_notInvokeCreateOverlayContainer() { mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG); - mMonitor.onStartActivity(mActivity, mIntent, createOverlayCreateParamsTestBundle()); + final Bundle optionsBundle = ActivityOptions.makeBasic().toBundle(); + optionsBundle.putString(KEY_OVERLAY_TAG, "test"); + mMonitor.onStartActivity(mActivity, mIntent, optionsBundle); verify(mSplitController, never()).createOrUpdateOverlayTaskFragmentIfNeeded(any(), any(), - anyInt(), any(), any()); - } - - @NonNull - private static Bundle createOverlayCreateParamsTestBundle() { - final Bundle bundle = new Bundle(); - - final Bundle paramsBundle = new Bundle(); - paramsBundle.putInt(KEY_OVERLAY_CREATE_PARAMS_TASK_ID, - TEST_OVERLAY_CREATE_PARAMS.getTaskId()); - paramsBundle.putString(KEY_OVERLAY_CREATE_PARAMS_TAG, TEST_OVERLAY_CREATE_PARAMS.getTag()); - paramsBundle.putObject(KEY_OVERLAY_CREATE_PARAMS_BOUNDS, - TEST_OVERLAY_CREATE_PARAMS.getBounds()); - - bundle.putBundle(KEY_OVERLAY_CREATE_PARAMS, paramsBundle); - - return bundle; + any(), any()); } @Test @@ -221,19 +194,11 @@ public class OverlayPresentationTest { } @Test - public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_taskIdNotMatch_throwException() { - assertThrows("The method must return null due to task mismatch between" - + " launchingActivity and OverlayCreateParams", IllegalArgumentException.class, - () -> createOrUpdateOverlayTaskFragmentIfNeeded( - TEST_OVERLAY_CREATE_PARAMS, TASK_ID + 1)); - } - - @Test public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask_dismissOverlay() { createExistingOverlayContainers(); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - new OverlayCreateParams(TASK_ID, "test3", new Rect(0, 0, 100, 100)), TASK_ID); + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test3"); assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is launched to the same task") @@ -245,9 +210,9 @@ public class OverlayPresentationTest { public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_sameTagAnotherTask_dismissOverlay() { createExistingOverlayContainers(); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - new OverlayCreateParams(TASK_ID + 2, "test1", new Rect(0, 0, 100, 100)), - TASK_ID + 2); + doReturn(TASK_ID + 2).when(mActivity).getTaskId(); + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test1"); assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is launched with the same tag as an existing overlay container in a different " @@ -261,9 +226,10 @@ public class OverlayPresentationTest { createExistingOverlayContainers(); final Rect bounds = new Rect(0, 0, 100, 100); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - new OverlayCreateParams(TASK_ID, "test1", bounds), - TASK_ID); + "test1"); assertWithMessage("overlayContainer1 must be updated since the new overlay container" + " is launched with the same tag and task") @@ -279,9 +245,8 @@ public class OverlayPresentationTest { public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissMultipleOverlays() { createExistingOverlayContainers(); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - new OverlayCreateParams(TASK_ID, "test2", new Rect(0, 0, 100, 100)), - TASK_ID); + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test2"); // OverlayContainer1 is dismissed since new container is launched in the same task with // different tag. OverlayContainer2 is dismissed since new container is launched with the @@ -304,8 +269,11 @@ public class OverlayPresentationTest { mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - TEST_OVERLAY_CREATE_PARAMS, TASK_ID); + final Rect bounds = new Rect(0, 0, 100, 100); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test"); final IBinder overlayToken = overlayContainer.getTaskFragmentToken(); assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) @@ -316,7 +284,7 @@ public class OverlayPresentationTest { // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case. clearInvocations(mSplitPresenter); - createOrUpdateOverlayTaskFragmentIfNeeded(TEST_OVERLAY_CREATE_PARAMS, TASK_ID); + createOrUpdateOverlayTaskFragmentIfNeeded("test"); verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect()); verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken, @@ -329,11 +297,11 @@ public class OverlayPresentationTest { public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_notInTaskBounds_expandOverlay() { final Rect bounds = new Rect(TASK_BOUNDS); bounds.offset(10, 10); - final OverlayCreateParams paramsOutsideTaskBounds = new OverlayCreateParams(TASK_ID, - "test", bounds); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - paramsOutsideTaskBounds, TASK_ID); + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test"); final IBinder overlayToken = overlayContainer.getTaskFragmentToken(); assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) @@ -344,7 +312,7 @@ public class OverlayPresentationTest { // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case. clearInvocations(mSplitPresenter); - createOrUpdateOverlayTaskFragmentIfNeeded(paramsOutsideTaskBounds, TASK_ID); + createOrUpdateOverlayTaskFragmentIfNeeded("test"); verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect()); verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken, @@ -355,15 +323,17 @@ public class OverlayPresentationTest { @Test public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_createOverlay() { - final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( - TEST_OVERLAY_CREATE_PARAMS, TASK_ID); + final Rect bounds = new Rect(0, 0, 100, 100); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test"); assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) .containsExactly(overlayContainer); assertThat(overlayContainer.getTaskId()).isEqualTo(TASK_ID); - assertThat(overlayContainer - .areLastRequestedBoundsEqual(TEST_OVERLAY_CREATE_PARAMS.getBounds())).isTrue(); - assertThat(overlayContainer.getOverlayTag()).isEqualTo(TEST_OVERLAY_CREATE_PARAMS.getTag()); + assertThat(overlayContainer.areLastRequestedBoundsEqual(bounds)).isTrue(); + assertThat(overlayContainer.getOverlayTag()).isEqualTo("test"); } @Test @@ -416,12 +386,14 @@ public class OverlayPresentationTest { @Test public void testGetTopNonFinishingActivityWithOverlay() { - createTestOverlayContainer(TASK_ID, "test1"); + TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); + final Activity activity = createMockActivity(); final TaskFragmentContainer container = createMockTaskFragmentContainer(activity); final TaskContainer task = container.getTaskContainer(); - assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)).isEqualTo(mActivity); + assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)) + .isEqualTo(overlayContainer.getTopNonFinishingActivity()); assertThat(task.getTopNonFinishingActivity(false /* includeOverlay */)).isEqualTo(activity); } @@ -458,10 +430,11 @@ public class OverlayPresentationTest { * #createOrUpdateOverlayTaskFragmentIfNeeded} */ @Nullable - private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded( - @NonNull OverlayCreateParams params, int taskId) { - return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, params, - taskId, mIntent, mActivity); + private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(@NonNull String tag) { + final Bundle launchOptions = new Bundle(); + launchOptions.putString(KEY_OVERLAY_TAG, tag); + return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction, + launchOptions, mIntent, mActivity); } /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @@ -475,10 +448,11 @@ public class OverlayPresentationTest { @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) { + Activity activity = createMockActivity(); TaskFragmentContainer overlayContainer = mSplitController.newContainer( - null /* pendingAppearedActivity */, mIntent, mActivity, taskId, - null /* pairedPrimaryContainer */, tag); - setupTaskFragmentInfo(overlayContainer, mActivity); + null /* pendingAppearedActivity */, mIntent, activity, taskId, + null /* pairedPrimaryContainer */, tag, Bundle.EMPTY); + setupTaskFragmentInfo(overlayContainer, activity); return overlayContainer; } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 8c274a26177d..bab4e9195880 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -590,7 +590,7 @@ public class SplitControllerTest { assertFalse(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString()); + anyString(), any()); } @Test @@ -753,7 +753,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString()); + anyString(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } @@ -796,7 +796,7 @@ public class SplitControllerTest { assertTrue(result); verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(), - anyString()); + anyString(), any()); verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any()); } diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 4511f3b91c5c..901d5fa0cd9a 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -57,3 +57,10 @@ flag { description: "Enables left/right split in portrait" bug: "291018646" } + +flag { + name: "enable_new_bubble_animations" + namespace: "multitasking" + description: "Enables new animations for expand and collapse for bubbles" + bug: "311450609" +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java index e06d3ef4e1ab..5b0de5070a60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationConstants.java @@ -21,5 +21,4 @@ package com.android.wm.shell.back; */ class BackAnimationConstants { static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f; - static final float PROGRESS_COMMIT_THRESHOLD = 0.1f; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index d8c691b01b61..a49823648d01 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -70,9 +70,11 @@ import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -124,6 +126,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final Context mContext; private final ContentResolver mContentResolver; private final ShellController mShellController; + private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; @@ -180,7 +183,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context, @NonNull BackAnimationBackground backAnimationBackground, - ShellBackAnimationRegistry shellBackAnimationRegistry) { + ShellBackAnimationRegistry shellBackAnimationRegistry, + ShellCommandHandler shellCommandHandler) { this( shellInit, shellController, @@ -190,7 +194,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont context, context.getContentResolver(), backAnimationBackground, - shellBackAnimationRegistry); + shellBackAnimationRegistry, + shellCommandHandler); } @VisibleForTesting @@ -203,7 +208,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont Context context, ContentResolver contentResolver, @NonNull BackAnimationBackground backAnimationBackground, - ShellBackAnimationRegistry shellBackAnimationRegistry) { + ShellBackAnimationRegistry shellBackAnimationRegistry, + ShellCommandHandler shellCommandHandler) { mShellController = shellController; mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; @@ -219,6 +225,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont .build(); mShellBackAnimationRegistry = shellBackAnimationRegistry; mLatencyTracker = LatencyTracker.getInstance(mContext); + mShellCommandHandler = shellCommandHandler; } private void onInit() { @@ -227,6 +234,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont createAdapter(); mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, this::createExternalInterface, this); + mShellCommandHandler.addDumpCallback(this::dump, this); } private void setupAnimationDeveloperSettingsObserver( @@ -968,4 +976,20 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont }; mBackAnimationAdapter = new BackAnimationAdapter(runner); } + + /** + * Description of current BackAnimationController state. + */ + private void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "BackAnimationController state:"); + pw.println(prefix + " mEnableAnimations=" + mEnableAnimations.get()); + pw.println(prefix + " mBackGestureStarted=" + mBackGestureStarted); + pw.println(prefix + " mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress); + pw.println(prefix + " mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent); + pw.println(prefix + " mCurrentTracker state:"); + mCurrentTracker.dump(pw, prefix + " "); + pw.println(prefix + " mQueuedTracker state:"); + mQueuedTracker.dump(pw, prefix + " "); + } + } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java index 215a6cc99e58..30d5edb59c85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java @@ -18,9 +18,9 @@ package com.android.wm.shell.back; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.window.BackEvent.EDGE_RIGHT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; -import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD; import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; @@ -91,7 +91,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { } }; private static final float MIN_WINDOW_ALPHA = 0.01f; - private static final float WINDOW_X_SHIFT_DP = 96; + private static final float WINDOW_X_SHIFT_DP = 48; private static final int SCALE_FACTOR = 100; // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists. private static final float TARGET_COMMIT_PROGRESS = 0.5f; @@ -126,6 +126,8 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); private boolean mBackInProgress = false; + private boolean mIsRightEdge; + private boolean mTriggerBack = false; private PointF mTouchPos = new PointF(); private IRemoteAnimationFinishedCallback mFinishCallback; @@ -209,6 +211,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private void finishAnimation() { if (mEnteringTarget != null) { + mTransaction.setCornerRadius(mEnteringTarget.leash, 0); mEnteringTarget.leash.release(); mEnteringTarget = null; } @@ -241,14 +244,15 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private void onGestureProgress(@NonNull BackEvent backEvent) { if (!mBackInProgress) { + mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); mBackInProgress = true; } mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); float progress = backEvent.getProgress(); - float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD - ? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1) + float springProgress = (mTriggerBack + ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1) : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; mLeavingProgressSpring.animateToFinalPosition(springProgress); mEnteringProgressSpring.animateToFinalPosition(springProgress); @@ -312,7 +316,7 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { transformWithProgress( mEnteringProgress, Math.max( - smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress), + smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress), MIN_WINDOW_ALPHA), /* alpha */ mEnteringTarget.leash, mEnteringRect, @@ -337,14 +341,13 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { mClosingTarget.leash, mClosingRect, 0, - mWindowXShift + mIsRightEdge ? 0 : mWindowXShift ); } } private void transformWithProgress(float progress, float alpha, SurfaceControl surface, RectF targetRect, float deltaXMin, float deltaXMax) { - final float touchY = mTouchPos.y; final int width = mStartTaskRect.width(); final int height = mStartTaskRect.height(); @@ -376,12 +379,14 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private final class Callback extends IOnBackInvokedCallback.Default { @Override public void onBackStarted(BackMotionEvent backEvent) { + mTriggerBack = backEvent.getTriggerBack(); mProgressAnimator.onBackStarted(backEvent, CrossActivityBackAnimation.this::onGestureProgress); } @Override public void onBackProgressed(@NonNull BackMotionEvent backEvent) { + mTriggerBack = backEvent.getTriggerBack(); mProgressAnimator.onBackProgressed(backEvent); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java index 4bd56d460818..6213f628dfd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -24,6 +24,8 @@ import android.view.RemoteAnimationTarget; import android.window.BackEvent; import android.window.BackMotionEvent; +import java.io.PrintWriter; + /** * Helper class to record the touch location for gesture and generate back events. */ @@ -129,6 +131,7 @@ class TouchTracker { /* progress = */ 0, /* velocityX = */ 0, /* velocityY = */ 0, + /* triggerBack = */ mTriggerBack, /* swipeEdge = */ mSwipeEdge, /* departingAnimationTarget = */ target); } @@ -204,6 +207,7 @@ class TouchTracker { /* progress = */ progress, /* velocityX = */ mLatestVelocityX, /* velocityY = */ mLatestVelocityY, + /* triggerBack = */ mTriggerBack, /* swipeEdge = */ mSwipeEdge, /* departingAnimationTarget = */ null); } @@ -219,6 +223,12 @@ class TouchTracker { mNonLinearFactor = nonLinearFactor; } + void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "TouchTracker state:"); + pw.println(prefix + " mState=" + mState); + pw.println(prefix + " mTriggerBack=" + mTriggerBack); + } + enum TouchTrackerState { INITIAL, ACTIVE, FINISHED } 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 249f52bd6156..896bcaf98599 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 @@ -1481,6 +1481,36 @@ public class BubbleController implements ConfigurationChangeListener, } } + // TODO(b/316358859): remove this method after task views are shared across modes + /** + * Removes the bubble with the given key after task removal, unless the task was removed as + * a result of mode switching, in which case, the bubble isn't removed because it will be + * re-inflated for the new mode. + */ + @MainThread + public void removeFloatingBubbleAfterTaskRemoval(String key, int reason) { + // if we're floating remove the bubble. otherwise, we're here because the task was removed + // after switching modes. See b/316358859 + if (!isShowingAsBubbleBar()) { + removeBubble(key, reason); + } + } + + // TODO(b/316358859): remove this method after task views are shared across modes + /** + * Removes the bubble with the given key after task removal, unless the task was removed as + * a result of mode switching, in which case, the bubble isn't removed because it will be + * re-inflated for the new mode. + */ + @MainThread + public void removeBarBubbleAfterTaskRemoval(String key, int reason) { + // if we're showing as bubble bar remove the bubble. otherwise, we're here because the task + // was removed after switching modes. See b/316358859 + if (isShowingAsBubbleBar()) { + removeBubble(key, reason); + } + } + /** * Removes all the bubbles. * <p> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt index e57f02c71e44..bd4708259b50 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt @@ -40,7 +40,13 @@ class BubbleEducationController(private val context: Context) { /** Whether education view should show for the collapsed stack. */ fun shouldShowStackEducation(bubble: BubbleViewProvider?): Boolean { - val shouldShow = bubble != null && + if (BubbleDebugConfig.neverShowUserEducation(context)) { + logDebug("Show stack edu: never") + return false + } + + val shouldShow = + bubble != null && bubble.isConversationBubble && // show education for conversation bubbles only (!hasSeenStackEducation || BubbleDebugConfig.forceShowUserEducation(context)) logDebug("Show stack edu: $shouldShow") @@ -49,7 +55,13 @@ class BubbleEducationController(private val context: Context) { /** Whether the educational view should show for the expanded view "manage" menu. */ fun shouldShowManageEducation(bubble: BubbleViewProvider?): Boolean { - val shouldShow = bubble != null && + if (BubbleDebugConfig.neverShowUserEducation(context)) { + logDebug("Show manage edu: never") + return false + } + + val shouldShow = + bubble != null && bubble.isConversationBubble && // show education for conversation bubbles only (!hasSeenManageEducation || BubbleDebugConfig.forceShowUserEducation(context)) logDebug("Show manage edu: $shouldShow") 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 a3eb429b1d7e..f3fe895bf9b4 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 @@ -313,7 +313,8 @@ public class BubbleExpandedView extends LinearLayout { + " bubble=" + getBubbleKey()); } if (mBubble != null) { - mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + mController.removeFloatingBubbleAfterTaskRemoval( + mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); } if (mTaskView != null) { // Release the surface 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 b7f749e8a8b6..470a82511481 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 @@ -1291,7 +1291,7 @@ public class BubbleStackView extends FrameLayout // We only show user education for conversation bubbles right now return false; } - final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); + final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION); final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null; if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { @@ -1342,7 +1342,7 @@ public class BubbleStackView extends FrameLayout // We only show user education for conversation bubbles right now return false; } - final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); + final boolean seen = getPrefBoolean(StackEducationView.PREF_STACK_EDUCATION); final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { Log.d(TAG, "Show stack edu: " + shouldShow); @@ -2323,7 +2323,8 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { - if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { + if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { maybeShowManageEdu(); } updateOverflowDotVisibility(true /* expanding */); @@ -2384,7 +2385,7 @@ public class BubbleStackView extends FrameLayout } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - if (mExpandedBubble.getExpandedView() != null) { + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { mExpandedBubble.getExpandedView().setContentAlpha(0f); mExpandedBubble.getExpandedView().setBackgroundAlpha(0f); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index da4a9898a44c..f6c382fb5b3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -188,7 +188,8 @@ public class BubbleTaskViewHelper { + " bubble=" + getBubbleKey()); } if (mBubble != null) { - mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + mController.removeBarBubbleAfterTaskRemoval( + mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt index 61e17c8ec459..da71b1c741bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -33,15 +33,16 @@ import com.android.wm.shell.animation.Interpolators * User education view to highlight the manage button that allows a user to configure the settings * for the bubble. Shown only the first time a user expands a bubble. */ -class ManageEducationView(context: Context, positioner: BubblePositioner) : LinearLayout(context) { - - private val TAG = - if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView" - else BubbleDebugConfig.TAG_BUBBLES - - private val ANIMATE_DURATION: Long = 200 +class ManageEducationView( + context: Context, + private val positioner: BubblePositioner +) : LinearLayout(context) { + + companion object { + const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding" + private const val ANIMATE_DURATION: Long = 200 + } - private val positioner: BubblePositioner = positioner private val manageView by lazy { requireViewById<ViewGroup>(R.id.manage_education_view) } private val manageButton by lazy { requireViewById<Button>(R.id.manage_button) } private val gotItButton by lazy { requireViewById<Button>(R.id.got_it) } @@ -128,7 +129,7 @@ class ManageEducationView(context: Context, positioner: BubblePositioner) : Line .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) } - setShouldShow(false) + updateManageEducationSeen() } /** @@ -218,13 +219,11 @@ class ManageEducationView(context: Context, positioner: BubblePositioner) : Line } } - private fun setShouldShow(shouldShow: Boolean) { + private fun updateManageEducationSeen() { context .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) .edit() - .putBoolean(PREF_MANAGED_EDUCATION, !shouldShow) + .putBoolean(PREF_MANAGED_EDUCATION, true) .apply() } } - -const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index 2cabb65abe7a..95f101722e89 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -34,19 +34,15 @@ import com.android.wm.shell.animation.Interpolators */ class StackEducationView( context: Context, - positioner: BubblePositioner, - controller: BubbleController + private val positioner: BubblePositioner, + private val controller: BubbleController ) : LinearLayout(context) { - private val TAG = - if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" - else BubbleDebugConfig.TAG_BUBBLES - - private val ANIMATE_DURATION: Long = 200 - private val ANIMATE_DURATION_SHORT: Long = 40 - - private val positioner: BubblePositioner = positioner - private val controller: BubbleController = controller + companion object { + const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding" + private const val ANIMATE_DURATION: Long = 200 + private const val ANIMATE_DURATION_SHORT: Long = 40 + } private val view by lazy { requireViewById<View>(R.id.stack_education_layout) } private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) } @@ -175,7 +171,7 @@ class StackEducationView( .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .alpha(1f) } - setShouldShow(false) + updateStackEducationSeen() return true } @@ -196,13 +192,11 @@ class StackEducationView( .withEndAction { visibility = GONE } } - private fun setShouldShow(shouldShow: Boolean) { + private fun updateStackEducationSeen() { context .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) .edit() - .putBoolean(PREF_STACK_EDUCATION, !shouldShow) + .putBoolean(PREF_STACK_EDUCATION, true) .apply() } } - -const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding" 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 5b0239f6d659..02af2d06a1dd 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 @@ -563,7 +563,7 @@ public class ExpandedAnimationController ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationX(fromX, p.y) + .translationX(fromX, p.x) .start(); } else { float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; @@ -634,4 +634,9 @@ public class ExpandedAnimationController .start(); } } + + /** Returns true if we're in the middle of a collapse or expand animation. */ + boolean isAnimating() { + return mAnimatingCollapse || mAnimatingExpand; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 7f34ee0cdd3d..893a87fe4885 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -23,6 +23,8 @@ import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Point; import android.util.Log; +import android.util.Size; +import android.view.View; import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -33,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject.MagneticTarget; /** * Helper class to animate a {@link BubbleBarExpandedView} on a bubble. @@ -44,6 +47,15 @@ public class BubbleBarAnimationHelper { private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f; private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; + private static final int EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION = 100; + private static final int EXPANDED_VIEW_ANIMATE_POSITION_DURATION = 300; + private static final int EXPANDED_VIEW_DISMISS_DURATION = 250; + private static final int EXPANDED_VIEW_DRAG_ANIMATION_DURATION = 150; + /** + * Additional scale applied to expanded view when it is positioned inside a magnetic target. + */ + private static final float EXPANDED_VIEW_IN_TARGET_SCALE = 0.6f; + private static final float EXPANDED_VIEW_DRAG_SCALE = 0.5f; /** Spring config for the expanded view scale-in animation. */ private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = @@ -62,6 +74,7 @@ public class BubbleBarAnimationHelper { private final Context mContext; private final BubbleBarLayerView mLayerView; private final BubblePositioner mPositioner; + private final int[] mTmpLocation = new int[2]; private BubbleViewProvider mExpandedBubble; private boolean mIsExpanded = false; @@ -181,7 +194,8 @@ public class BubbleBarAnimationHelper { Log.w(TAG, "Trying to animate collapse without a bubble"); return; } - + bbev.setScaleX(1f); + bbev.setScaleY(1f); mExpandedViewContainerMatrix.setScaleX(1f); mExpandedViewContainerMatrix.setScaleY(1f); @@ -209,11 +223,173 @@ public class BubbleBarAnimationHelper { } /** + * Animate the expanded bubble when it is being dragged + */ + public void animateStartDrag() { + final BubbleBarExpandedView bbev = getExpandedView(); + if (bbev == null) { + Log.w(TAG, "Trying to animate start drag without a bubble"); + return; + } + bbev.setPivotX(bbev.getWidth() / 2f); + bbev.setPivotY(0f); + bbev.animate() + .scaleX(EXPANDED_VIEW_DRAG_SCALE) + .scaleY(EXPANDED_VIEW_DRAG_SCALE) + .setInterpolator(Interpolators.EMPHASIZED) + .setDuration(EXPANDED_VIEW_DRAG_ANIMATION_DURATION) + .start(); + } + + /** + * Animates dismissal of currently expanded bubble + * + * @param endRunnable a runnable to run at the end of the animation + */ + public void animateDismiss(Runnable endRunnable) { + mIsExpanded = false; + final BubbleBarExpandedView bbev = getExpandedView(); + if (bbev == null) { + Log.w(TAG, "Trying to animate dismiss without a bubble"); + return; + } + + int[] location = bbev.getLocationOnScreen(); + int diffFromBottom = mPositioner.getScreenRect().bottom - location[1]; + + bbev.animate() + // 2x distance from bottom so the view flies out + .translationYBy(diffFromBottom * 2) + .setDuration(EXPANDED_VIEW_DISMISS_DURATION) + .withEndAction(endRunnable) + .start(); + } + + /** + * Animate current expanded bubble back to its rest position + */ + public void animateToRestPosition() { + BubbleBarExpandedView bbev = getExpandedView(); + if (bbev == null) { + Log.w(TAG, "Trying to animate expanded view to rest position without a bubble"); + return; + } + Point restPoint = getExpandedViewRestPosition(getExpandedViewSize()); + bbev.animate() + .x(restPoint.x) + .y(restPoint.y) + .scaleX(1f) + .scaleY(1f) + .setDuration(EXPANDED_VIEW_ANIMATE_POSITION_DURATION) + .setInterpolator(Interpolators.EMPHASIZED_DECELERATE) + .withStartAction(() -> bbev.setAnimating(true)) + .withEndAction(() -> { + bbev.setAnimating(false); + bbev.resetPivot(); + }) + .start(); + } + + /** + * Animates currently expanded bubble into the given {@link MagneticTarget}. + * + * @param target magnetic target to snap to + * @param endRunnable a runnable to run at the end of the animation + */ + public void animateIntoTarget(MagneticTarget target, @Nullable Runnable endRunnable) { + BubbleBarExpandedView bbev = getExpandedView(); + if (bbev == null) { + Log.w(TAG, "Trying to snap the expanded view to target without a bubble"); + return; + } + + // Calculate scale of expanded view so it fits inside the magnetic target + float bbevMaxSide = Math.max(bbev.getWidth(), bbev.getHeight()); + View targetView = target.getTargetView(); + float targetMaxSide = Math.max(targetView.getWidth(), targetView.getHeight()); + // Reduce target size to have some padding between the target and expanded view + targetMaxSide *= EXPANDED_VIEW_IN_TARGET_SCALE; + float scaleInTarget = targetMaxSide / bbevMaxSide; + + // Scale around the top center of the expanded view. Same as when dragging. + bbev.setPivotX(bbev.getWidth() / 2f); + bbev.setPivotY(0); + + // When the view animates into the target, it is scaled down with the pivot at center top. + // Find the point on the view that would be the center of the view at its final scale. + // Once we know that, we can calculate x and y distance from the center of the target view + // and use that for the translation animation to ensure that the view at final scale is + // placed at the center of the target. + + // Set mTmpLocation to the current location of the view on the screen, taking into account + // any scale applied. + bbev.getLocationOnScreen(mTmpLocation); + // Since pivotX is at the center of the x-axis, even at final scale, center of the view on + // x-axis will be the same as the center of the view at current size. + // Get scaled width of the view and adjust mTmpLocation so that point on x-axis is at the + // center of the view at its current size. + float currentWidth = bbev.getWidth() * bbev.getScaleX(); + mTmpLocation[0] += currentWidth / 2; + // Since pivotY is at the top of the view, at final scale, top coordinate of the view + // remains the same. + // Get height of the view at final scale and adjust mTmpLocation so that point on y-axis is + // moved down by half of the height at final scale. + float targetHeight = bbev.getHeight() * scaleInTarget; + mTmpLocation[1] += targetHeight / 2; + // mTmpLocation is now set to the point on the view that will be the center of the view once + // scale is applied. + + // Calculate the difference between the target's center coordinates and mTmpLocation + float xDiff = target.getCenterOnScreen().x - mTmpLocation[0]; + float yDiff = target.getCenterOnScreen().y - mTmpLocation[1]; + + bbev.animate() + .translationX(bbev.getTranslationX() + xDiff) + .translationY(bbev.getTranslationY() + yDiff) + .scaleX(scaleInTarget) + .scaleY(scaleInTarget) + .setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION) + .setInterpolator(Interpolators.EMPHASIZED) + .withStartAction(() -> bbev.setAnimating(true)) + .withEndAction(() -> { + bbev.setAnimating(false); + if (endRunnable != null) { + endRunnable.run(); + } + }) + .start(); + } + + /** + * Animate currently expanded view when it is released from dismiss view + */ + public void animateUnstuckFromDismissView() { + BubbleBarExpandedView expandedView = getExpandedView(); + if (expandedView == null) { + Log.w(TAG, "Trying to unsnap the expanded view from dismiss without a bubble"); + return; + } + expandedView + .animate() + .scaleX(EXPANDED_VIEW_DRAG_SCALE) + .scaleY(EXPANDED_VIEW_DRAG_SCALE) + .setDuration(EXPANDED_VIEW_SNAP_TO_DISMISS_DURATION) + .setInterpolator(Interpolators.EMPHASIZED) + .withStartAction(() -> expandedView.setAnimating(true)) + .withEndAction(() -> expandedView.setAnimating(false)) + .start(); + } + + /** * Cancel current animations */ public void cancelAnimations() { PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); mExpandedViewAlphaAnimator.cancel(); + BubbleBarExpandedView bbev = getExpandedView(); + if (bbev != null) { + bbev.animate().cancel(); + } } private @Nullable BubbleBarExpandedView getExpandedView() { @@ -231,21 +407,34 @@ public class BubbleBarAnimationHelper { return; } - boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY); - final int padding = mPositioner.getBubbleBarExpandedViewPadding(); - final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); - final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); + final Size size = getExpandedViewSize(); + Point position = getExpandedViewRestPosition(size); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams(); - lp.width = width; - lp.height = height; + lp.width = size.getWidth(); + lp.height = size.getHeight(); bbev.setLayoutParams(lp); + bbev.setX(position.x); + bbev.setY(position.y); + bbev.updateLocation(); + bbev.maybeShowOverflow(); + } + + private Point getExpandedViewRestPosition(Size size) { + final int padding = mPositioner.getBubbleBarExpandedViewPadding(); + Point point = new Point(); if (mLayerView.isOnLeft()) { - bbev.setX(mPositioner.getInsets().left + padding); + point.x = mPositioner.getInsets().left + padding; } else { - bbev.setX(mPositioner.getAvailableRect().width() - width - padding); + point.x = mPositioner.getAvailableRect().width() - size.getWidth() - padding; } - bbev.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); - bbev.updateLocation(); - bbev.maybeShowOverflow(); + point.y = mPositioner.getExpandedViewBottomForBubbleBar() - size.getHeight(); + return point; + } + + private Size getExpandedViewSize() { + boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY); + final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); + final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); + return new Size(width, height); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index 4ea18f78f5b2..5e634a23955a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -16,70 +16,70 @@ package com.android.wm.shell.bubbles.bar -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.graphics.PointF -import android.graphics.Rect +import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View -import com.android.wm.shell.animation.Interpolators import com.android.wm.shell.common.bubbles.DismissView import com.android.wm.shell.common.bubbles.RelativeTouchListener +import com.android.wm.shell.common.magnetictarget.MagnetizedObject /** Controller for handling drag interactions with [BubbleBarExpandedView] */ +@SuppressLint("ClickableViewAccessibility") class BubbleBarExpandedViewDragController( private val expandedView: BubbleBarExpandedView, private val dismissView: DismissView, + private val animationHelper: BubbleBarAnimationHelper, private val onDismissed: () -> Unit ) { + var isStuckToDismiss: Boolean = false + private set + + private var expandedViewInitialTranslationX = 0f + private var expandedViewInitialTranslationY = 0f + private val magnetizedExpandedView: MagnetizedObject<BubbleBarExpandedView> = + MagnetizedObject.magnetizeView(expandedView) + private val magnetizedDismissTarget: MagnetizedObject.MagneticTarget + init { - expandedView.handleView.setOnTouchListener(HandleDragListener()) - } + magnetizedExpandedView.magnetListener = MagnetListener() + magnetizedExpandedView.animateStuckToTarget = + { + target: MagnetizedObject.MagneticTarget, + _: Float, + _: Float, + _: Boolean, + after: (() -> Unit)? -> + animationHelper.animateIntoTarget(target, after) + } - private fun finishDrag(x: Float, y: Float, viewInitialX: Float, viewInitialY: Float) { - val dismissCircleBounds = Rect().apply { dismissView.circle.getBoundsOnScreen(this) } - if (dismissCircleBounds.contains(x.toInt(), y.toInt())) { - onDismissed() - } else { - resetExpandedViewPosition(viewInitialX, viewInitialY) - } - dismissView.hide() - } + magnetizedDismissTarget = + MagnetizedObject.MagneticTarget(dismissView.circle, dismissView.circle.width) + magnetizedExpandedView.addTarget(magnetizedDismissTarget) - private fun resetExpandedViewPosition(initialX: Float, initialY: Float) { - val listener = - object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - expandedView.isAnimating = true - } + val dragMotionEventHandler = HandleDragListener() - override fun onAnimationEnd(animation: Animator) { - expandedView.isAnimating = false - } + expandedView.handleView.setOnTouchListener { view, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + expandedViewInitialTranslationX = expandedView.translationX + expandedViewInitialTranslationY = expandedView.translationY } - expandedView - .animate() - .translationX(initialX) - .translationY(initialY) - .setDuration(RESET_POSITION_ANIM_DURATION) - .setInterpolator(Interpolators.EMPHASIZED_DECELERATE) - .setListener(listener) - .start() + val magnetConsumed = magnetizedExpandedView.maybeConsumeMotionEvent(event) + // Move events can be consumed by the magnetized object + if (event.actionMasked == MotionEvent.ACTION_MOVE && magnetConsumed) { + return@setOnTouchListener true + } + return@setOnTouchListener dragMotionEventHandler.onTouch(view, event) || magnetConsumed + } } private inner class HandleDragListener : RelativeTouchListener() { - private val expandedViewRestPosition = PointF() + private var isMoving = false override fun onDown(v: View, ev: MotionEvent): Boolean { // While animating, don't allow new touch events - if (expandedView.isAnimating) { - return false - } - expandedViewRestPosition.x = expandedView.translationX - expandedViewRestPosition.y = expandedView.translationY - return true + return !expandedView.isAnimating } override fun onMove( @@ -90,8 +90,12 @@ class BubbleBarExpandedViewDragController( dx: Float, dy: Float ) { - expandedView.translationX = expandedViewRestPosition.x + dx - expandedView.translationY = expandedViewRestPosition.y + dy + if (!isMoving) { + isMoving = true + animationHelper.animateStartDrag() + } + expandedView.translationX = expandedViewInitialTranslationX + dx + expandedView.translationY = expandedViewInitialTranslationY + dy dismissView.show() } @@ -105,16 +109,41 @@ class BubbleBarExpandedViewDragController( velX: Float, velY: Float ) { - finishDrag(ev.rawX, ev.rawY, expandedViewRestPosition.x, expandedViewRestPosition.y) + finishDrag() } override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) { - resetExpandedViewPosition(expandedViewRestPosition.x, expandedViewRestPosition.y) - dismissView.hide() + finishDrag() + } + + private fun finishDrag() { + if (!isStuckToDismiss) { + animationHelper.animateToRestPosition() + dismissView.hide() + } + isMoving = false } } - companion object { - const val RESET_POSITION_ANIM_DURATION = 300L + private inner class MagnetListener : MagnetizedObject.MagnetListener { + override fun onStuckToTarget(target: MagnetizedObject.MagneticTarget) { + isStuckToDismiss = true + } + + override fun onUnstuckFromTarget( + target: MagnetizedObject.MagneticTarget, + velX: Float, + velY: Float, + wasFlungOut: Boolean + ) { + isStuckToDismiss = false + animationHelper.animateUnstuckFromDismissView() + } + + override fun onReleasedInTarget(target: MagnetizedObject.MagneticTarget) { + onDismissed() + dismissView.hide() + } } } + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index bdb0e206e490..12114519d086 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles.bar; import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE; import android.annotation.Nullable; import android.content.Context; @@ -36,7 +37,6 @@ import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; -import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.bubbles.DismissView; @@ -206,10 +206,13 @@ public class BubbleBarLayerView extends FrameLayout } }); - mDragController = new BubbleBarExpandedViewDragController(mExpandedView, mDismissView, + mDragController = new BubbleBarExpandedViewDragController( + mExpandedView, + mDismissView, + mAnimationHelper, () -> { mBubbleController.dismissBubble(mExpandedBubble.getKey(), - Bubbles.DISMISS_USER_GESTURE); + DISMISS_USER_GESTURE); return Unit.INSTANCE; }); @@ -241,7 +244,11 @@ public class BubbleBarLayerView extends FrameLayout mIsExpanded = false; final BubbleBarExpandedView viewToRemove = mExpandedView; mEducationViewController.hideEducation(/* animated = */ true); - mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); + if (mDragController != null && mDragController.isStuckToDismiss()) { + mAnimationHelper.animateDismiss(() -> removeView(viewToRemove)); + } else { + mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); + } mBubbleController.getSysuiProxy().onStackExpandChanged(false); mExpandedView = null; mDragController = null; 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 3c6bc1754c5c..fc97c7988a0f 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 @@ -363,7 +363,8 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor shellExecutor, @ShellBackgroundThread Handler backgroundHandler, BackAnimationBackground backAnimationBackground, - Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry) { + Optional<ShellBackAnimationRegistry> shellBackAnimationRegistry, + ShellCommandHandler shellCommandHandler) { if (BackAnimationController.IS_ENABLED) { return shellBackAnimationRegistry.map( (animations) -> @@ -374,7 +375,8 @@ public abstract class WMShellBaseModule { backgroundHandler, context, backAnimationBackground, - animations)); + animations, + shellCommandHandler)); } return Optional.empty(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 0448d94669ce..0b8f60e44c7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -96,7 +96,7 @@ public class PipScheduler { @Nullable private WindowContainerTransaction getExitPipViaExpandTransaction() { - if (mPipTaskToken == null || mPinnedTaskLeash == null) { + if (mPipTaskToken == null) { return null; } WindowContainerTransaction wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 6200ea583a48..48a0a46dccc1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -18,6 +18,7 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; @@ -56,6 +57,8 @@ public class PipTransition extends PipTransitionController { private IBinder mAutoEnterButtonNavTransition; @Nullable private IBinder mExitViaExpandTransition; + @Nullable + private IBinder mLegacyEnterTransition; public PipTransition( @NonNull ShellInit shellInit, @@ -98,6 +101,9 @@ public class PipTransition extends PipTransitionController { if (isAutoEnterInButtonNavigation(request)) { mAutoEnterButtonNavTransition = transition; return getEnterPipTransaction(transition, request); + } else if (isLegacyEnter(request)) { + mLegacyEnterTransition = transition; + return getEnterPipTransaction(transition, request); } return null; } @@ -108,6 +114,9 @@ public class PipTransition extends PipTransitionController { if (isAutoEnterInButtonNavigation(request)) { outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */); mAutoEnterButtonNavTransition = transition; + } else if (isLegacyEnter(request)) { + outWct.merge(getEnterPipTransaction(transition, request), true /* transfer */); + mLegacyEnterTransition = transition; } } @@ -153,6 +162,10 @@ public class PipTransition extends PipTransitionController { && pipTask.pictureInPictureParams.isAutoEnterEnabled(); } + private boolean isLegacyEnter(@NonNull TransitionRequestInfo requestInfo) { + return requestInfo.getType() == TRANSIT_PIP; + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -161,29 +174,65 @@ public class PipTransition extends PipTransitionController { @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition == mAutoEnterButtonNavTransition) { mAutoEnterButtonNavTransition = null; - TransitionInfo.Change pipChange = getPipChange(info); - if (pipChange == null) { - return false; - } - mPipTaskToken = pipChange.getContainer(); - - // cache the PiP task token and leash - mPipScheduler.setPipTaskToken(mPipTaskToken); - mPipScheduler.setPinnedTaskLeash(pipChange.getLeash()); - - startTransaction.apply(); - finishCallback.onTransitionFinished(null); - return true; + return startAutoEnterButtonNavAnimation(info, startTransaction, finishTransaction, + finishCallback); + } else if (transition == mLegacyEnterTransition) { + mLegacyEnterTransition = null; + return startLegacyEnterAnimation(info, startTransaction, finishTransaction, + finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; - startTransaction.apply(); - finishCallback.onTransitionFinished(null); - onExitPip(); - return true; + return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); } return false; } + private boolean startAutoEnterButtonNavAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + TransitionInfo.Change pipChange = getPipChange(info); + if (pipChange == null) { + return false; + } + mPipTaskToken = pipChange.getContainer(); + + // cache the PiP task token and leash + mPipScheduler.setPipTaskToken(mPipTaskToken); + + startTransaction.apply(); + finishCallback.onTransitionFinished(null); + return true; + } + + private boolean startLegacyEnterAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + TransitionInfo.Change pipChange = getPipChange(info); + if (pipChange == null) { + return false; + } + mPipTaskToken = pipChange.getContainer(); + + // cache the PiP task token and leash + mPipScheduler.setPipTaskToken(mPipTaskToken); + + startTransaction.apply(); + finishCallback.onTransitionFinished(null); + return true; + } + + private boolean startExpandAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + startTransaction.apply(); + finishCallback.onTransitionFinished(null); + onExitPip(); + return true; + } + @Nullable private TransitionInfo.Change getPipChange(TransitionInfo info) { for (TransitionInfo.Change change : info.getChanges()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index ae21c4bf5450..f58aeac918b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -277,11 +277,6 @@ public class SplashscreenContentDrawer { params.token = appToken; params.packageName = activityInfo.packageName; params.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - - if (!context.getResources().getCompatibilityInfo().supportsScreen()) { - params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW; - } - params.setTitle("Splash Screen " + title); return params; } 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 af69b5272ad5..b0d8b47b170a 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 @@ -757,6 +757,10 @@ public class Transitions implements RemoteCallable<Transitions>, } if (!change.hasFlags(FLAG_IS_OCCLUDED)) { allOccluded = false; + } else if (change.hasAllFlags(TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION)) { + // Remove the change because it should be invisible in the animation. + info.getChanges().remove(i); + continue; } // The change has already animated by back gesture, don't need to play transition // animation on it. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 771876f7ce5d..9ded6ea1d187 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -63,6 +63,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; +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.ShellSharedConstants; @@ -110,6 +111,8 @@ public class BackAnimationControllerTest extends ShellTestCase { @Mock private InputManager mInputManager; + @Mock + private ShellCommandHandler mShellCommandHandler; private BackAnimationController mController; private TestableContentResolver mContentResolver; @@ -145,7 +148,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mContext, mContentResolver, mAnimationBackground, - mShellBackAnimationRegistry); + mShellBackAnimationRegistry, + mShellCommandHandler); mShellInit.init(); mShellExecutor.flushAll(); } @@ -298,7 +302,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mContext, mContentResolver, mAnimationBackground, - mShellBackAnimationRegistry); + mShellBackAnimationRegistry, + mShellCommandHandler); shellInit.init(); registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 874ef80c29f0..91503b1c3619 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -53,6 +53,7 @@ public class BackProgressAnimatorTest { /* progress = */ progress, /* velocityX = */ 0, /* velocityY = */ 0, + /* triggerBack = */ false, /* swipeEdge = */ BackEvent.EDGE_LEFT, /* departingAnimationTarget = */ null); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java index c1ff260836b8..60f1d271c3af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java @@ -16,52 +16,51 @@ package com.android.wm.shell.bubbles.animation; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.annotation.SuppressLint; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.PointF; import android.graphics.Rect; -import android.testing.AndroidTestingRunner; import android.view.View; import android.view.WindowManager; import android.widget.FrameLayout; import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleStackView; +import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + @SmallTest -@RunWith(AndroidTestingRunner.class) +@RunWith(AndroidJUnit4.class) public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase { - private int mDisplayWidth = 500; - private int mDisplayHeight = 1000; - - private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class); + private final Semaphore mBubbleRemovedSemaphore = new Semaphore(0); + private final Runnable mOnBubbleAnimatedOutAction = mBubbleRemovedSemaphore::release; ExpandedAnimationController mExpandedController; private int mStackOffset; private PointF mExpansionPoint; private BubblePositioner mPositioner; - private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState(); + private final BubbleStackView.StackViewState mStackViewState = + new BubbleStackView.StackViewState(); - @SuppressLint("VisibleForTests") @Before public void setUp() throws Exception { super.setUp(); @@ -70,15 +69,13 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC getContext().getSystemService(WindowManager.class)); mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, Insets.of(0, 0, 0, 0), - new Rect(0, 0, mDisplayWidth, mDisplayHeight)); + new Rect(0, 0, 500, 1000)); BubbleStackView stackView = mock(BubbleStackView.class); - when(stackView.getState()).thenReturn(getStackViewState()); mExpandedController = new ExpandedAnimationController(mPositioner, mOnBubbleAnimatedOutAction, stackView); - spyOn(mExpandedController); addOneMoreThanBubbleLimitBubbles(); mLayout.setActiveController(mExpandedController); @@ -86,9 +83,18 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC Resources res = mLayout.getResources(); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); mExpansionPoint = new PointF(100, 100); + + getStackViewState(); + when(stackView.getState()).thenAnswer(i -> getStackViewState()); + waitForMainThread(); } - public BubbleStackView.StackViewState getStackViewState() { + @After + public void tearDown() { + waitForMainThread(); + } + + private BubbleStackView.StackViewState getStackViewState() { mStackViewState.numberOfBubbles = mLayout.getChildCount(); mStackViewState.selectedIndex = 0; mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint); @@ -96,68 +102,71 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC } @Test - @Ignore - public void testExpansionAndCollapse() throws InterruptedException { - Runnable afterExpand = mock(Runnable.class); - mExpandedController.expandFromStack(afterExpand); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - + public void testExpansionAndCollapse() throws Exception { + expand(); testBubblesInCorrectExpandedPositions(); - verify(afterExpand).run(); + waitForMainThread(); - Runnable afterCollapse = mock(Runnable.class); + final Semaphore semaphore = new Semaphore(0); + Runnable afterCollapse = semaphore::release; mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); - - testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1); - verify(afterExpand).run(); + assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue(); + waitForAnimation(); + testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y); } @Test - @Ignore - public void testOnChildAdded() throws InterruptedException { + public void testOnChildAdded() throws Exception { expand(); + waitForMainThread(); // Add another new view and wait for its animation. final View newView = new FrameLayout(getContext()); mLayout.addView(newView, 0); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + waitForAnimation(); testBubblesInCorrectExpandedPositions(); } @Test - @Ignore - public void testOnChildRemoved() throws InterruptedException { + public void testOnChildRemoved() throws Exception { expand(); + waitForMainThread(); - // Remove some views and see if the remaining child views still pass the expansion test. + // Remove some views and verify the remaining child views still pass the expansion test. mLayout.removeView(mViews.get(0)); mLayout.removeView(mViews.get(3)); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Removing a view will invoke onBubbleAnimatedOutAction. Block until it gets called twice. + assertThat(mBubbleRemovedSemaphore.tryAcquire(2, 2, TimeUnit.SECONDS)).isTrue(); + + waitForAnimation(); testBubblesInCorrectExpandedPositions(); } @Test - public void testDragBubbleOutDoesntNPE() throws InterruptedException { + public void testDragBubbleOutDoesntNPE() { mExpandedController.onGestureFinished(); mExpandedController.dragBubbleOut(mViews.get(0), 1, 1); } /** Expand the stack and wait for animations to finish. */ private void expand() throws InterruptedException { - mExpandedController.expandFromStack(mock(Runnable.class)); - waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + final Semaphore semaphore = new Semaphore(0); + Runnable afterExpand = semaphore::release; + + mExpandedController.expandFromStack(afterExpand); + assertThat(semaphore.tryAcquire(1, TimeUnit.SECONDS)).isTrue(); } /** Check that children are in the correct positions for being stacked. */ - private void testStackedAtPosition(float x, float y, int offsetMultiplier) { + private void testStackedAtPosition(float x, float y) { // Make sure the rest of the stack moved again, including the first bubble not moving, and // is stacked to the right now that we're on the right side of the screen. for (int i = 0; i < mLayout.getChildCount(); i++) { - assertEquals(x + i * offsetMultiplier * mStackOffset, - mLayout.getChildAt(i).getTranslationX(), 2f); - assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f); + assertEquals(x, mLayout.getChildAt(i).getTranslationX(), 2f); + assertEquals(y + Math.min(i, 1) * mStackOffset, mLayout.getChildAt(i).getTranslationY(), + 2f); assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f); } } @@ -175,4 +184,22 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC mLayout.getChildAt(i).getTranslationY(), 2f); } } + + private void waitForAnimation() throws Exception { + final Semaphore semaphore = new Semaphore(0); + boolean[] animating = new boolean[]{ true }; + for (int i = 0; i < 4; i++) { + if (animating[0]) { + mMainThreadHandler.post(() -> { + if (!mExpandedController.isAnimating()) { + animating[0] = false; + semaphore.release(); + } + }); + Thread.sleep(500); + } + } + assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue(); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java index 48ae2961b4be..2ed5addd900c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java @@ -164,11 +164,17 @@ public class PhysicsAnimationLayoutTestCase extends ShellTestCase { @Override public void cancelAllAnimations() { + if (mLayout.getChildCount() == 0) { + return; + } mMainThreadHandler.post(super::cancelAllAnimations); } @Override public void cancelAnimationsOnView(View view) { + if (mLayout.getChildCount() == 0) { + return; + } mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view)); } @@ -221,6 +227,9 @@ public class PhysicsAnimationLayoutTestCase extends ShellTestCase { @Override protected void startPathAnimation() { + if (mLayout.getChildCount() == 0) { + return; + } mMainThreadHandler.post(super::startPathAnimation); } } @@ -322,4 +331,9 @@ public class PhysicsAnimationLayoutTestCase extends ShellTestCase { e.printStackTrace(); } } + + /** Waits for the main thread to finish processing all pending runnables. */ + public void waitForMainThread() { + runOnMainThreadAndBlock(() -> {}); + } } diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 94f35fd9eaf2..facf30b83b07 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -37,6 +37,9 @@ // Android-specific addition that is used to show when frames began in systrace EGLAPI void EGLAPIENTRY eglBeginFrame(EGLDisplay dpy, EGLSurface surface); +static constexpr auto P3_XRB = static_cast<android_dataspace>( + ADATASPACE_STANDARD_DCI_P3 | ADATASPACE_TRANSFER_SRGB | ADATASPACE_RANGE_EXTENDED); + namespace android { namespace uirenderer { namespace renderthread { @@ -497,9 +500,7 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, // This relies on knowing that EGL will not re-set the dataspace after the call to // eglCreateWindowSurface. Since the handling of the colorspace extension is largely // implemented in libEGL in the platform, we can safely assume this is the case - int32_t err = ANativeWindow_setBuffersDataSpace( - window, - static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED)); + int32_t err = ANativeWindow_setBuffersDataSpace(window, P3_XRB); LOG_ALWAYS_FATAL_IF(err, "Failed to ANativeWindow_setBuffersDataSpace %d", err); } diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp index 20b743bab2c2..a8e85475aff0 100644 --- a/libs/hwui/renderthread/VulkanSurface.cpp +++ b/libs/hwui/renderthread/VulkanSurface.cpp @@ -29,6 +29,9 @@ namespace android { namespace uirenderer { namespace renderthread { +static constexpr auto P3_XRB = static_cast<android_dataspace>( + ADATASPACE_STANDARD_DCI_P3 | ADATASPACE_TRANSFER_SRGB | ADATASPACE_RANGE_EXTENDED); + static int InvertTransform(int transform) { switch (transform) { case ANATIVEWINDOW_TRANSFORM_ROTATE_90: @@ -214,8 +217,7 @@ bool VulkanSurface::InitializeWindowInfoStruct(ANativeWindow* window, ColorMode outWindowInfo->colorMode = colorMode; if (colorMode == ColorMode::Hdr || colorMode == ColorMode::Hdr10) { - outWindowInfo->dataspace = - static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED); + outWindowInfo->dataspace = P3_XRB; } else { outWindowInfo->dataspace = ColorSpaceToADataSpace(colorSpace.get(), colorType); } @@ -541,8 +543,7 @@ void VulkanSurface::setColorSpace(sk_sp<SkColorSpace> colorSpace) { } if (mWindowInfo.colorMode == ColorMode::Hdr || mWindowInfo.colorMode == ColorMode::Hdr10) { - mWindowInfo.dataspace = - static_cast<android_dataspace>(STANDARD_DCI_P3 | TRANSFER_SRGB | RANGE_EXTENDED); + mWindowInfo.dataspace = P3_XRB; } else { mWindowInfo.dataspace = ColorSpaceToADataSpace( mWindowInfo.colorspace.get(), BufferFormatToColorType(mWindowInfo.bufferFormat)); |