diff options
Diffstat (limited to 'libs')
255 files changed, 18163 insertions, 2840 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java deleted file mode 100644 index b7a60392c512..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2021 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; - -import android.content.Context; - -/** - * Provider class that will instantiate the library implementation. It must be included in the - * vendor library, and the vendor implementation must match the signature of this class. - */ -public class ExtensionProvider { - /** - * Provides a simple implementation of {@link ExtensionInterface} that can be replaced by - * an OEM by overriding this method. - */ - public static ExtensionInterface getExtensionImpl(Context context) { - return new SampleExtensionImpl(context); - } - - /** - * The support library will use this method to check API version compatibility. - * @return API version string in MAJOR.MINOR.PATCH-description format. - */ - public static String getApiVersion() { - return "1.0.0-settings_sample"; - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java deleted file mode 100644 index 6a53efee0e74..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/StubExtension.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2021 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; - -import android.app.Activity; - -import androidx.annotation.NonNull; - -import java.util.HashSet; -import java.util.Set; - -/** - * Basic implementation of the {@link ExtensionInterface}. An OEM can choose to use it as the base - * class for their implementation. - */ -abstract class StubExtension implements ExtensionInterface { - - private ExtensionCallback mExtensionCallback; - private final Set<Activity> mWindowLayoutChangeListenerActivities = new HashSet<>(); - - StubExtension() { - } - - @Override - public void setExtensionCallback(@NonNull ExtensionCallback extensionCallback) { - this.mExtensionCallback = extensionCallback; - } - - @Override - public void onWindowLayoutChangeListenerAdded(@NonNull Activity activity) { - this.mWindowLayoutChangeListenerActivities.add(activity); - this.onListenersChanged(); - } - - @Override - public void onWindowLayoutChangeListenerRemoved(@NonNull Activity activity) { - this.mWindowLayoutChangeListenerActivities.remove(activity); - this.onListenersChanged(); - } - - void updateWindowLayout(@NonNull Activity activity, - @NonNull ExtensionWindowLayoutInfo newLayout) { - if (this.mExtensionCallback != null) { - mExtensionCallback.onWindowLayoutChanged(activity, newLayout); - } - } - - @NonNull - Set<Activity> getActivitiesListeningForLayoutChanges() { - return mWindowLayoutChangeListenerActivities; - } - - protected boolean hasListeners() { - return !mWindowLayoutChangeListenerActivities.isEmpty(); - } - - protected abstract void onListenersChanged(); -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java new file mode 100644 index 000000000000..bdf703c9bd38 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 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; + +import android.app.ActivityThread; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.window.extensions.embedding.ActivityEmbeddingComponent; +import androidx.window.extensions.embedding.SplitController; +import androidx.window.extensions.layout.WindowLayoutComponent; +import androidx.window.extensions.layout.WindowLayoutComponentImpl; + +/** + * The reference implementation of {@link WindowExtensions} that implements the initial API version. + */ +public class WindowExtensionsImpl implements WindowExtensions { + + private final Object mLock = new Object(); + private volatile WindowLayoutComponent mWindowLayoutComponent; + private volatile SplitController mSplitController; + + @Override + public int getVendorApiLevel() { + return 1; + } + + /** + * Returns a reference implementation of {@link WindowLayoutComponent} if available, + * {@code null} otherwise. The implementation must match the API level reported in + * {@link WindowExtensions#getWindowLayoutComponent()}. + * @return {@link WindowLayoutComponent} OEM implementation + */ + @Override + public WindowLayoutComponent getWindowLayoutComponent() { + if (mWindowLayoutComponent == null) { + synchronized (mLock) { + if (mWindowLayoutComponent == null) { + Context context = ActivityThread.currentApplication(); + mWindowLayoutComponent = new WindowLayoutComponentImpl(context); + } + } + } + return mWindowLayoutComponent; + } + + /** + * Returns a reference implementation of {@link ActivityEmbeddingComponent} if available, + * {@code null} otherwise. The implementation must match the API level reported in + * {@link WindowExtensions#getWindowLayoutComponent()}. + * @return {@link ActivityEmbeddingComponent} OEM implementation. + */ + @NonNull + public ActivityEmbeddingComponent getActivityEmbeddingComponent() { + if (mSplitController == null) { + synchronized (mLock) { + if (mSplitController == null) { + mSplitController = new SplitController(); + } + } + } + return mSplitController; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java new file mode 100644 index 000000000000..f9e1f077cffc --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 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; + +import android.annotation.NonNull; + +/** + * Provides the OEM implementation of {@link WindowExtensions}. + */ +public class WindowExtensionsProvider { + + private static final WindowExtensions sWindowExtensions = new WindowExtensionsImpl(); + + /** + * Returns the OEM implementation of {@link WindowExtensions}. This method is implemented in + * the library provided on the device and overwrites one in the Jetpack library included in + * apps. + * @return the OEM implementation of {@link WindowExtensions} + */ + @NonNull + public static WindowExtensions getWindowExtensions() { + return sWindowExtensions; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java new file mode 100644 index 000000000000..85ef270ac49d --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2021 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 android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.app.Activity; +import android.app.WindowConfiguration.WindowingMode; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.TaskFragmentAppearedInfo; +import android.window.TaskFragmentCreationParams; +import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOrganizer; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Platform default Extensions implementation of {@link TaskFragmentOrganizer} to organize + * task fragments. + * + * All calls into methods of this class are expected to be on the UI thread. + */ +class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { + + /** Mapping from the client assigned unique token to the {@link TaskFragmentInfo}. */ + private final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>(); + + /** Mapping from the client assigned unique token to the TaskFragment {@link SurfaceControl}. */ + private final Map<IBinder, SurfaceControl> mFragmentLeashes = new ArrayMap<>(); + + /** + * Mapping from the client assigned unique token to the TaskFragment parent + * {@link Configuration}. + */ + final Map<IBinder, Configuration> mFragmentParentConfigs = new ArrayMap<>(); + + private final TaskFragmentCallback mCallback; + private TaskFragmentAnimationController mAnimationController; + + /** + * Callback that notifies the controller about changes to task fragments. + */ + interface TaskFragmentCallback { + void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo); + void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo); + void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo); + void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, + @NonNull Configuration parentConfig); + } + + /** + * @param executor callbacks from WM Core are posted on this executor. It should be tied to the + * UI thread that all other calls into methods of this class are also on. + */ + JetpackTaskFragmentOrganizer(@NonNull Executor executor, TaskFragmentCallback callback) { + super(executor); + mCallback = callback; + } + + @Override + public void registerOrganizer() { + if (mAnimationController != null) { + throw new IllegalStateException("Must unregister the organizer before re-register."); + } + super.registerOrganizer(); + mAnimationController = new TaskFragmentAnimationController(this); + mAnimationController.registerRemoteAnimations(); + } + + @Override + public void unregisterOrganizer() { + if (mAnimationController != null) { + mAnimationController.unregisterRemoteAnimations(); + mAnimationController = null; + } + super.unregisterOrganizer(); + } + + /** + * Starts a new Activity and puts it into split with an existing Activity side-by-side. + * @param launchingFragmentToken token for the launching TaskFragment. If it exists, it will + * be resized based on {@param launchingFragmentBounds}. + * Otherwise, we will create a new TaskFragment with the given + * token for the {@param launchingActivity}. + * @param launchingFragmentBounds the initial bounds for the launching TaskFragment. + * @param launchingActivity the Activity to put on the left hand side of the split as the + * primary. + * @param secondaryFragmentToken token to create the secondary TaskFragment with. + * @param secondaryFragmentBounds the initial bounds for the secondary TaskFragment + * @param activityIntent Intent to start the secondary Activity with. + * @param activityOptions ActivityOptions to start the secondary Activity with. + */ + void startActivityToSide(@NonNull WindowContainerTransaction wct, + @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds, + @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken, + @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent, + @Nullable Bundle activityOptions, @NonNull SplitRule rule) { + final IBinder ownerToken = launchingActivity.getActivityToken(); + + // Create or resize the launching TaskFragment. + if (mFragmentInfos.containsKey(launchingFragmentToken)) { + resizeTaskFragment(wct, launchingFragmentToken, launchingFragmentBounds); + } else { + createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken, + launchingFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, launchingActivity); + } + + // Create a TaskFragment for the secondary activity. + createTaskFragmentAndStartActivity(wct, secondaryFragmentToken, ownerToken, + secondaryFragmentBounds, WINDOWING_MODE_MULTI_WINDOW, activityIntent, + activityOptions); + + // Set adjacent to each other so that the containers below will be invisible. + setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule); + } + + /** + * Expands an existing TaskFragment to fill parent. + * @param wct WindowContainerTransaction in which the task fragment should be resized. + * @param fragmentToken token of an existing TaskFragment. + */ + void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { + resizeTaskFragment(wct, fragmentToken, new Rect()); + setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */); + } + + /** + * Expands an existing TaskFragment to fill parent. + * @param fragmentToken token of an existing TaskFragment. + */ + void expandTaskFragment(IBinder fragmentToken) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + expandTaskFragment(wct, fragmentToken); + applyTransaction(wct); + } + + /** + * Expands an Activity to fill parent by moving it to a new TaskFragment. + * @param fragmentToken token to create new TaskFragment with. + * @param activity activity to move to the fill-parent TaskFragment. + */ + void expandActivity(IBinder fragmentToken, Activity activity) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + createTaskFragmentAndReparentActivity( + wct, fragmentToken, activity.getActivityToken(), new Rect(), + WINDOWING_MODE_UNDEFINED, activity); + applyTransaction(wct); + } + + /** + * @param ownerToken The token of the activity that creates this task fragment. It does not + * have to be a child of this task fragment, but must belong to the same task. + */ + void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken, + IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) { + final TaskFragmentCreationParams fragmentOptions = + createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode); + wct.createTaskFragment(fragmentOptions); + } + + /** + * @param ownerToken The token of the activity that creates this task fragment. It does not + * have to be a child of this task fragment, but must belong to the same task. + */ + private void createTaskFragmentAndReparentActivity( + WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken, + @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) { + createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); + wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken()); + } + + /** + * @param ownerToken The token of the activity that creates this task fragment. It does not + * have to be a child of this task fragment, but must belong to the same task. + */ + private void createTaskFragmentAndStartActivity( + WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken, + @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent, + @Nullable Bundle activityOptions) { + createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode); + wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions); + } + + void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, + @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) { + WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null; + final boolean finishSecondaryWithPrimary = + splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule); + final boolean finishPrimaryWithSecondary = + splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule); + if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) { + adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams(); + adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary); + adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary); + } + wct.setAdjacentTaskFragments(primary, secondary, adjacentParams); + } + + TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken, + Rect bounds, @WindowingMode int windowingMode) { + if (mFragmentInfos.containsKey(fragmentToken)) { + throw new IllegalArgumentException( + "There is an existing TaskFragment with fragmentToken=" + fragmentToken); + } + + return new TaskFragmentCreationParams.Builder( + getOrganizerToken(), + fragmentToken, + ownerToken) + .setInitialBounds(bounds) + .setWindowingMode(windowingMode) + .build(); + } + + void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken, + @Nullable Rect bounds) { + if (!mFragmentInfos.containsKey(fragmentToken)) { + throw new IllegalArgumentException( + "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); + } + if (bounds == null) { + bounds = new Rect(); + } + wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds); + } + + void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) { + if (!mFragmentInfos.containsKey(fragmentToken)) { + throw new IllegalArgumentException( + "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); + } + wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken()); + } + + @Override + public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) { + final TaskFragmentInfo info = taskFragmentAppearedInfo.getTaskFragmentInfo(); + final IBinder fragmentToken = info.getFragmentToken(); + final SurfaceControl leash = taskFragmentAppearedInfo.getLeash(); + mFragmentInfos.put(fragmentToken, info); + mFragmentLeashes.put(fragmentToken, leash); + + if (mCallback != null) { + mCallback.onTaskFragmentAppeared(taskFragmentAppearedInfo); + } + } + + @Override + public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { + final IBinder fragmentToken = taskFragmentInfo.getFragmentToken(); + mFragmentInfos.put(fragmentToken, taskFragmentInfo); + + if (mCallback != null) { + mCallback.onTaskFragmentInfoChanged(taskFragmentInfo); + } + } + + @Override + public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { + mFragmentInfos.remove(taskFragmentInfo.getFragmentToken()); + mFragmentLeashes.remove(taskFragmentInfo.getFragmentToken()); + mFragmentParentConfigs.remove(taskFragmentInfo.getFragmentToken()); + + if (mCallback != null) { + mCallback.onTaskFragmentVanished(taskFragmentInfo); + } + } + + @Override + public void onTaskFragmentParentInfoChanged( + @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) { + mFragmentParentConfigs.put(fragmentToken, parentConfig); + + if (mCallback != null) { + mCallback.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java new file mode 100644 index 000000000000..06e7d1457417 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 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 android.annotation.NonNull; +import android.app.Activity; + +/** + * Client-side descriptor of a split that holds two containers. + */ +class SplitContainer { + private final TaskFragmentContainer mPrimaryContainer; + private final TaskFragmentContainer mSecondaryContainer; + private final SplitRule mSplitRule; + + SplitContainer(@NonNull TaskFragmentContainer primaryContainer, + @NonNull Activity primaryActivity, + @NonNull TaskFragmentContainer secondaryContainer, + @NonNull SplitRule splitRule) { + mPrimaryContainer = primaryContainer; + mSecondaryContainer = secondaryContainer; + mSplitRule = splitRule; + + if (shouldFinishPrimaryWithSecondary(splitRule)) { + if (mPrimaryContainer.getRunningActivityCount() == 1 + && mPrimaryContainer.hasActivity(primaryActivity.getActivityToken())) { + mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer); + } else { + // Only adding the activity to be finished vs. the entire TaskFragment while + // the secondary container exits because there are other unrelated activities in the + // primary TaskFragment. + mSecondaryContainer.addActivityToFinishOnExit(primaryActivity); + } + } + if (shouldFinishSecondaryWithPrimary(splitRule)) { + mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer); + } + } + + @NonNull + TaskFragmentContainer getPrimaryContainer() { + return mPrimaryContainer; + } + + @NonNull + TaskFragmentContainer getSecondaryContainer() { + return mSecondaryContainer; + } + + @NonNull + SplitRule getSplitRule() { + return mSplitRule; + } + + boolean isPlaceholderContainer() { + return (mSplitRule instanceof SplitPlaceholderRule); + } + + static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { + final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; + final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule) + && ((SplitPairRule) splitRule).shouldFinishPrimaryWithSecondary(); + return shouldFinishPrimaryWithSecondary || isPlaceholderContainer; + } + + static boolean shouldFinishSecondaryWithPrimary(@NonNull SplitRule splitRule) { + final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule; + final boolean shouldFinishSecondaryWithPrimary = (splitRule instanceof SplitPairRule) + && ((SplitPairRule) splitRule).shouldFinishSecondaryWithPrimary(); + return shouldFinishSecondaryWithPrimary || isPlaceholderContainer; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java new file mode 100644 index 000000000000..42b438041d7a --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -0,0 +1,806 @@ +/* + * Copyright (C) 2021 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.ActivityClient; +import android.app.ActivityOptions; +import android.app.ActivityThread; +import android.app.Application.ActivityLifecycleCallbacks; +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.window.TaskFragmentAppearedInfo; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerTransaction; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Main controller class that manages split states and presentation. + */ +public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, + ActivityEmbeddingComponent { + + private final SplitPresenter mPresenter; + + // Currently applied split configuration. + private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); + private final List<TaskFragmentContainer> mContainers = new ArrayList<>(); + private final List<SplitContainer> mSplitContainers = new ArrayList<>(); + + // Callback to Jetpack to notify about changes to split states. + private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback; + private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); + + public SplitController() { + mPresenter = new SplitPresenter(new MainThreadExecutor(), this); + ActivityThread activityThread = ActivityThread.currentActivityThread(); + // Register a callback to be notified about activities being created. + activityThread.getApplication().registerActivityLifecycleCallbacks( + new LifecycleCallbacks()); + // Intercept activity starts to route activities to new containers if necessary. + Instrumentation instrumentation = activityThread.getInstrumentation(); + instrumentation.addMonitor(new ActivityStartMonitor()); + } + + /** Updates the embedding rules applied to future activity launches. */ + @Override + public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { + mSplitRules.clear(); + mSplitRules.addAll(rules); + } + + @NonNull + public List<EmbeddingRule> getSplitRules() { + return mSplitRules; + } + + /** + * Starts an activity to side of the launchingActivity with the provided split config. + */ + public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, + @Nullable Bundle options, @NonNull SplitRule sideRule, + @NonNull Consumer<Exception> failureCallback) { + try { + mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule); + } catch (Exception e) { + failureCallback.accept(e); + } + } + + /** + * Registers the split organizer callback to notify about changes to active splits. + */ + @Override + public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { + mEmbeddingCallback = callback; + updateCallbackIfNecessary(); + } + + @Override + public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) { + TaskFragmentContainer container = getContainer( + taskFragmentAppearedInfo.getTaskFragmentInfo().getFragmentToken()); + if (container == null) { + return; + } + + container.setInfo(taskFragmentAppearedInfo.getTaskFragmentInfo()); + if (container.isFinished()) { + mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); + } + updateCallbackIfNecessary(); + } + + @Override + public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { + TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } + + container.setInfo(taskFragmentInfo); + // Check if there are no running activities - consider the container empty if there are no + // non-finishing activities left. + if (!taskFragmentInfo.hasRunningActivity()) { + // Do not finish the dependents if this TaskFragment was cleared due to launching + // activity in the Task. + final boolean shouldFinishDependent = + !taskFragmentInfo.isTaskClearedForReuse(); + mPresenter.cleanupContainer(container, shouldFinishDependent); + } + updateCallbackIfNecessary(); + } + + @Override + public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { + TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); + if (container == null) { + return; + } + + mPresenter.cleanupContainer(container, true /* shouldFinishDependent */); + updateCallbackIfNecessary(); + } + + @Override + public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, + @NonNull Configuration parentConfig) { + TaskFragmentContainer container = getContainer(fragmentToken); + if (container != null) { + mPresenter.updateContainer(container); + updateCallbackIfNecessary(); + } + } + + void onActivityCreated(@NonNull Activity launchedActivity) { + handleActivityCreated(launchedActivity); + updateCallbackIfNecessary(); + } + + /** + * Checks if the activity start should be routed to a particular container. It can create a new + * container for the activity and a new split container if necessary. + */ + // TODO(b/190433398): Break down into smaller functions. + void handleActivityCreated(@NonNull Activity launchedActivity) { + final List<EmbeddingRule> splitRules = getSplitRules(); + final TaskFragmentContainer currentContainer = getContainerWithActivity( + launchedActivity.getActivityToken()); + + // Check if the activity is configured to always be expanded. + if (shouldExpand(launchedActivity, null, splitRules)) { + if (shouldContainerBeExpanded(currentContainer)) { + // Make sure that the existing container is expanded + mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); + } else { + // Put activity into a new expanded container + final TaskFragmentContainer newContainer = newContainer(launchedActivity); + mPresenter.expandActivity(newContainer.getTaskFragmentToken(), + launchedActivity); + } + return; + } + + // Check if activity requires a placeholder + if (launchPlaceholderIfNecessary(launchedActivity)) { + return; + } + + // TODO(b/190433398): Check if it is a placeholder and there is already another split + // created by the primary activity. This is necessary for the case when the primary activity + // launched another secondary in the split, but the placeholder was still launched by the + // logic above. We didn't prevent the placeholder launcher because we didn't know that + // another secondary activity is coming up. + + // Check if the activity should form a split with the activity below in the same task + // fragment. + Activity activityBelow = null; + if (currentContainer != null) { + final List<Activity> containerActivities = currentContainer.collectActivities(); + final int index = containerActivities.indexOf(launchedActivity); + if (index > 0) { + activityBelow = containerActivities.get(index - 1); + } + } + if (activityBelow == null) { + IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( + launchedActivity.getActivityToken()); + if (belowToken != null) { + activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); + } + } + if (activityBelow == null) { + return; + } + + // Check if the split is already set. + final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( + activityBelow.getActivityToken()); + if (currentContainer != null && activityBelowContainer != null) { + final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, + activityBelowContainer); + if (existingSplit != null) { + // There is already an active split with the activity below. + return; + } + } + + final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, + splitRules); + if (splitPairRule == null) { + return; + } + + mPresenter.createNewSplitContainer(activityBelow, launchedActivity, + splitPairRule); + } + + private void onActivityConfigurationChanged(@NonNull Activity activity) { + final TaskFragmentContainer currentContainer = getContainerWithActivity( + activity.getActivityToken()); + + if (currentContainer != null) { + // Changes to activities in controllers are handled in + // onTaskFragmentParentInfoChanged + return; + } + + // Check if activity requires a placeholder + launchPlaceholderIfNecessary(activity); + } + + /** + * Returns a container that this activity is registered with. An activity can only belong to one + * container, or no container at all. + */ + @Nullable + TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { + for (TaskFragmentContainer container : mContainers) { + if (container.hasActivity(activityToken)) { + return container; + } + } + + return null; + } + + /** + * Creates and registers a new organized container with an optional activity that will be + * re-parented to it in a WCT. + */ + TaskFragmentContainer newContainer(@Nullable Activity activity) { + TaskFragmentContainer container = new TaskFragmentContainer(activity); + mContainers.add(container); + return container; + } + + /** + * Creates and registers a new split with the provided containers and configuration. Finishes + * existing secondary containers if found for the given primary container. + */ + void registerSplit(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, + @NonNull TaskFragmentContainer secondaryContainer, + @NonNull SplitRule splitRule) { + if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { + removeExistingSecondaryContainers(wct, primaryContainer); + } + SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, + secondaryContainer, splitRule); + mSplitContainers.add(splitContainer); + } + + /** + * Removes the container from bookkeeping records. + */ + void removeContainer(@NonNull TaskFragmentContainer container) { + // Remove all split containers that included this one + mContainers.remove(container); + List<SplitContainer> containersToRemove = new ArrayList<>(); + for (SplitContainer splitContainer : mSplitContainers) { + if (container.equals(splitContainer.getSecondaryContainer()) + || container.equals(splitContainer.getPrimaryContainer())) { + containersToRemove.add(splitContainer); + } + } + mSplitContainers.removeAll(containersToRemove); + } + + /** + * Removes a secondary container for the given primary container if an existing split is + * already registered. + */ + void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer primaryContainer) { + // If the primary container was already in a split - remove the secondary container that + // is now covered by the new one that replaced it. + final SplitContainer existingSplitContainer = getActiveSplitForContainer( + primaryContainer); + if (existingSplitContainer == null + || primaryContainer == existingSplitContainer.getSecondaryContainer()) { + return; + } + + existingSplitContainer.getSecondaryContainer().finish( + false /* shouldFinishDependent */, mPresenter, wct, this); + } + + /** + * Returns the topmost not finished container. + */ + @Nullable + TaskFragmentContainer getTopActiveContainer() { + for (int i = mContainers.size() - 1; i >= 0; i--) { + TaskFragmentContainer container = mContainers.get(i); + if (!container.isFinished()) { + return container; + } + } + return null; + } + + /** + * Updates the presentation of the container. If the container is part of the split or should + * have a placeholder, it will also update the other part of the split. + */ + void updateContainer(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { + if (launchPlaceholderIfNecessary(container)) { + // Placeholder was launched, the positions will be updated when the activity is added + // to the secondary container. + return; + } + if (shouldContainerBeExpanded(container)) { + if (container.getInfo() != null) { + mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + } + // If the info is not available yet the task fragment will be expanded when it's ready + return; + } + SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer == null) { + return; + } + if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) { + // Skip position update - it isn't the topmost split. + return; + } + if (splitContainer.getPrimaryContainer().isEmpty() + || splitContainer.getSecondaryContainer().isEmpty()) { + // Skip position update - one or both containers are empty. + return; + } + if (dismissPlaceholderIfNecessary(splitContainer)) { + // Placeholder was finished, the positions will be updated when its container is emptied + return; + } + mPresenter.updateSplitContainer(splitContainer, container, wct); + } + + /** + * Returns the top active split container that has the provided container, if available. + */ + @Nullable + private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + SplitContainer splitContainer = mSplitContainers.get(i); + if (container.equals(splitContainer.getSecondaryContainer()) + || container.equals(splitContainer.getPrimaryContainer())) { + return splitContainer; + } + } + return null; + } + + /** + * Returns the active split that has the provided containers as primary and secondary or as + * secondary and primary, if available. + */ + @Nullable + private SplitContainer getActiveSplitForContainers( + @NonNull TaskFragmentContainer firstContainer, + @NonNull TaskFragmentContainer secondContainer) { + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + SplitContainer splitContainer = mSplitContainers.get(i); + final TaskFragmentContainer primary = splitContainer.getPrimaryContainer(); + final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer(); + if ((firstContainer == secondary && secondContainer == primary) + || (firstContainer == primary && secondContainer == secondary)) { + return splitContainer; + } + } + return null; + } + + /** + * Checks if the container requires a placeholder and launches it if necessary. + */ + private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) { + final Activity topActivity = container.getTopNonFinishingActivity(); + if (topActivity == null) { + return false; + } + + return launchPlaceholderIfNecessary(topActivity); + } + + boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { + final TaskFragmentContainer container = getContainerWithActivity( + activity.getActivityToken()); + + SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) + : null; + if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { + // Don't launch placeholder in primary split container + return false; + } + + // Check if there is enough space for launch + final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity); + if (placeholderRule == null || !mPresenter.shouldShowSideBySide( + mPresenter.getParentContainerBounds(activity), placeholderRule)) { + return false; + } + + // TODO(b/190433398): Handle failed request + startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null, + placeholderRule, null); + return true; + } + + private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { + if (!splitContainer.isPlaceholderContainer()) { + return false; + } + + if (mPresenter.shouldShowSideBySide(splitContainer)) { + return false; + } + + mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(), + false /* shouldFinishDependent */); + return true; + } + + /** + * Returns the rule to launch a placeholder for the activity with the provided component name + * if it is configured in the split config. + */ + private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { + for (EmbeddingRule rule : mSplitRules) { + if (!(rule instanceof SplitPlaceholderRule)) { + continue; + } + SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule; + if (placeholderRule.matchesActivity(activity)) { + return placeholderRule; + } + } + return null; + } + + /** + * Notifies listeners about changes to split states if necessary. + */ + private void updateCallbackIfNecessary() { + if (mEmbeddingCallback == null) { + return; + } + if (!allActivitiesCreated()) { + return; + } + List<SplitInfo> currentSplitStates = getActiveSplitStates(); + if (mLastReportedSplitStates.equals(currentSplitStates)) { + return; + } + mLastReportedSplitStates.clear(); + mLastReportedSplitStates.addAll(currentSplitStates); + mEmbeddingCallback.accept(currentSplitStates); + } + + /** + * Returns a list of descriptors for currently active split states. + */ + private List<SplitInfo> getActiveSplitStates() { + List<SplitInfo> splitStates = new ArrayList<>(); + for (SplitContainer container : mSplitContainers) { + if (container.getPrimaryContainer().isEmpty() + || container.getSecondaryContainer().isEmpty()) { + // Skipping containers that do not have any activities to report. + continue; + } + ActivityStack primaryContainer = + new ActivityStack( + container.getPrimaryContainer().collectActivities()); + ActivityStack secondaryContainer = + new ActivityStack( + container.getSecondaryContainer().collectActivities()); + SplitInfo splitState = new SplitInfo(primaryContainer, + secondaryContainer, + // Splits that are not showing side-by-side are reported as having 0 split + // ratio, since by definition in the API the primary container occupies no + // width of the split when covered by the secondary. + mPresenter.shouldShowSideBySide(container) + ? container.getSplitRule().getSplitRatio() + : 0.0f); + splitStates.add(splitState); + } + return splitStates; + } + + /** + * Checks if all activities that are registered with the containers have already appeared in + * the client. + */ + private boolean allActivitiesCreated() { + for (TaskFragmentContainer container : mContainers) { + if (container.getInfo() == null + || container.getInfo().getActivities().size() + != container.collectActivities().size()) { + return false; + } + } + return true; + } + + /** + * Returns {@code true} if the container is expanded to occupy full task size. + * Returns {@code false} if the container is included in an active split. + */ + boolean shouldContainerBeExpanded(@Nullable TaskFragmentContainer container) { + if (container == null) { + return false; + } + for (SplitContainer splitContainer : mSplitContainers) { + if (container.equals(splitContainer.getPrimaryContainer()) + || container.equals(splitContainer.getSecondaryContainer())) { + return false; + } + } + return true; + } + + /** + * Returns a split rule for the provided pair of primary activity and secondary activity intent + * if available. + */ + @Nullable + private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { + for (EmbeddingRule rule : splitRules) { + if (!(rule instanceof SplitPairRule)) { + continue; + } + SplitPairRule pairRule = (SplitPairRule) rule; + if (pairRule.matchesActivityIntentPair(primaryActivity, secondaryActivityIntent)) { + return pairRule; + } + } + return null; + } + + /** + * Returns a split rule for the provided pair of primary and secondary activities if available. + */ + @Nullable + private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { + for (EmbeddingRule rule : splitRules) { + if (!(rule instanceof SplitPairRule)) { + continue; + } + SplitPairRule pairRule = (SplitPairRule) rule; + final Intent intent = secondaryActivity.getIntent(); + if (pairRule.matchesActivityPair(primaryActivity, secondaryActivity) + && (intent == null + || pairRule.matchesActivityIntentPair(primaryActivity, intent))) { + return pairRule; + } + } + return null; + } + + @Nullable + TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) { + for (TaskFragmentContainer container : mContainers) { + if (container.getTaskFragmentToken().equals(fragmentToken)) { + return container; + } + } + return null; + } + + /** + * Returns {@code true} if an Activity with the provided component name should always be + * expanded to occupy full task bounds. Such activity must not be put in a split. + */ + private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent, + List<EmbeddingRule> splitRules) { + if (splitRules == null) { + return false; + } + for (EmbeddingRule rule : splitRules) { + if (!(rule instanceof ActivityRule)) { + continue; + } + ActivityRule activityRule = (ActivityRule) rule; + if (!activityRule.shouldAlwaysExpand()) { + continue; + } + if (activity != null && activityRule.matchesActivity(activity)) { + return true; + } else if (intent != null && activityRule.matchesIntent(intent)) { + return true; + } + } + return false; + } + + private final class LifecycleCallbacks implements ActivityLifecycleCallbacks { + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { + // Calling after Activity#onCreate is complete to allow the app launch something + // first. In case of a configured placeholder activity we want to make sure + // that we don't launch it if an activity itself already requested something to be + // launched to side. + SplitController.this.onActivityCreated(activity); + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityResumed(Activity activity) { + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } + + @Override + public void onActivityConfigurationChanged(Activity activity) { + SplitController.this.onActivityConfigurationChanged(activity); + } + } + + /** Executor that posts on the main application thread. */ + private static class MainThreadExecutor implements Executor { + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable r) { + mHandler.post(r); + } + } + + /** + * A monitor that intercepts all activity start requests originating in the client process and + * can amend them to target a specific task fragment to form a split. + */ + private class ActivityStartMonitor extends Instrumentation.ActivityMonitor { + + @Override + public Instrumentation.ActivityResult onStartActivity(@NonNull Context who, + @NonNull Intent intent, @NonNull Bundle options) { + // TODO(b/190433398): Check if the activity is configured to always be expanded. + + // Check if activity should be put in a split with the activity that launched it. + if (!(who instanceof Activity)) { + return super.onStartActivity(who, intent, options); + } + final Activity launchingActivity = (Activity) who; + + if (shouldExpand(null, intent, getSplitRules())) { + setLaunchingInExpandedContainer(launchingActivity, options); + } else if (!setLaunchingToSideContainer(launchingActivity, intent, options)) { + setLaunchingInSameContainer(launchingActivity, intent, options); + } + + return super.onStartActivity(who, intent, options); + } + + private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { + TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( + launchingActivity); + + // Amend the request to let the WM know that the activity should be placed in the + // dedicated container. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + newContainer.getTaskFragmentToken()); + } + + /** + * Returns {@code true} if the activity that is going to be started via the + * {@code intent} should be paired with the {@code launchingActivity} and is set to be + * launched in an empty side container. + */ + private boolean setLaunchingToSideContainer(Activity launchingActivity, Intent intent, + Bundle options) { + final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, + getSplitRules()); + if (splitPairRule == null) { + return false; + } + + // Create a new split with an empty side container + final TaskFragmentContainer secondaryContainer = mPresenter + .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); + + // Amend the request to let the WM know that the activity should be placed in the + // dedicated container. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + secondaryContainer.getTaskFragmentToken()); + return true; + } + + /** + * Checks if the activity that is going to be started via the {@code intent} should be + * paired with the existing top activity which is currently paired with the + * {@code launchingActivity}. If so, set the activity to be launched in the same + * container of the {@code launchingActivity}. + */ + private void setLaunchingInSameContainer(Activity launchingActivity, Intent intent, + Bundle options) { + final TaskFragmentContainer launchingContainer = getContainerWithActivity( + launchingActivity.getActivityToken()); + if (launchingContainer == null) { + return; + } + + final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); + if (splitContainer == null) { + return; + } + + if (splitContainer.getSecondaryContainer() != launchingContainer) { + return; + } + + // The launching activity is on the secondary container. Retrieve the primary + // activity from the other container. + Activity primaryActivity = + splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); + if (primaryActivity == null) { + return; + } + + final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, + getSplitRules()); + if (splitPairRule == null) { + return; + } + + // Amend the request to let the WM know that the activity should be placed in the + // dedicated container. This is necessary for the case that the activity is started + // into a new Task, or new Task will be escaped from the current host Task and be + // displayed in fullscreen. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + launchingContainer.getTaskFragmentToken()); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java new file mode 100644 index 000000000000..81be21cbd7aa --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2021 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 android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.util.LayoutDirection; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowMetrics; +import android.window.TaskFragmentCreationParams; +import android.window.WindowContainerTransaction; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.concurrent.Executor; + +/** + * Controls the visual presentation of the splits according to the containers formed by + * {@link SplitController}. + */ +class SplitPresenter extends JetpackTaskFragmentOrganizer { + private static final int POSITION_START = 0; + private static final int POSITION_END = 1; + private static final int POSITION_FILL = 2; + + @IntDef(value = { + POSITION_START, + POSITION_END, + POSITION_FILL, + }) + private @interface Position {} + + private final SplitController mController; + + SplitPresenter(@NonNull Executor executor, SplitController controller) { + super(executor, controller); + mController = controller; + registerOrganizer(); + } + + /** + * Updates the presentation of the provided container. + */ + void updateContainer(TaskFragmentContainer container) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mController.updateContainer(wct, container); + applyTransaction(wct); + } + + /** + * Deletes the specified container and all other associated and dependent containers in the same + * transaction. + */ + void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + container.finish(shouldFinishDependent, this, wct, mController); + + final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(); + if (newTopContainer != null) { + mController.updateContainer(wct, newTopContainer); + } + + applyTransaction(wct); + } + + /** + * Creates a new split with the primary activity and an empty secondary container. + * @return The newly created secondary container. + */ + TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity, + @NonNull SplitPairRule rule) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + final Rect parentBounds = getParentContainerBounds(primaryActivity); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, + isLtr(primaryActivity, rule)); + final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, + primaryActivity, primaryRectBounds, null); + + // Create new empty task fragment + final TaskFragmentContainer secondaryContainer = mController.newContainer(null); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, + rule, isLtr(primaryActivity, rule)); + createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), + primaryActivity.getActivityToken(), secondaryRectBounds, + WINDOWING_MODE_MULTI_WINDOW); + secondaryContainer.setLastRequestedBounds(secondaryRectBounds); + + // Set adjacent to each other so that the containers below will be invisible. + setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), + secondaryContainer.getTaskFragmentToken(), rule); + + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + + applyTransaction(wct); + + return secondaryContainer; + } + + /** + * Creates a new split container with the two provided activities. + * @param primaryActivity An activity that should be in the primary container. If it is not + * currently in an existing container, a new one will be created and the + * activity will be re-parented to it. + * @param secondaryActivity An activity that should be in the secondary container. If it is not + * currently in an existing container, or if it is currently in the + * same container as the primary activity, a new container will be + * created and the activity will be re-parented to it. + * @param rule The split rule to be applied to the container. + */ + void createNewSplitContainer(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + final Rect parentBounds = getParentContainerBounds(primaryActivity); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, + isLtr(primaryActivity, rule)); + final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, + primaryActivity, primaryRectBounds, null); + + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, + isLtr(primaryActivity, rule)); + final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, + secondaryActivity, secondaryRectBounds, primaryContainer); + + // Set adjacent to each other so that the containers below will be invisible. + setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), + secondaryContainer.getTaskFragmentToken(), rule); + + mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); + + applyTransaction(wct); + } + + /** + * Creates a new expanded container. + */ + TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { + final TaskFragmentContainer newContainer = mController.newContainer(null); + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + createTaskFragment(wct, newContainer.getTaskFragmentToken(), + launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_MULTI_WINDOW); + + applyTransaction(wct); + return newContainer; + } + + /** + * Creates a new container or resizes an existing container for activity to the provided bounds. + * @param activity The activity to be re-parented to the container if necessary. + * @param containerToAvoid Re-parent from this container if an activity is already in it. + */ + private TaskFragmentContainer prepareContainerForActivity( + @NonNull WindowContainerTransaction wct, @NonNull Activity activity, + @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { + TaskFragmentContainer container = mController.getContainerWithActivity( + activity.getActivityToken()); + if (container == null || container == containerToAvoid) { + container = mController.newContainer(activity); + + final TaskFragmentCreationParams fragmentOptions = + createFragmentOptions( + container.getTaskFragmentToken(), + activity.getActivityToken(), + bounds, + WINDOWING_MODE_MULTI_WINDOW); + wct.createTaskFragment(fragmentOptions); + + wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), + activity.getActivityToken()); + + container.setLastRequestedBounds(bounds); + } else { + resizeTaskFragmentIfRegistered(wct, container, bounds); + } + + return container; + } + + /** + * Starts a new activity to the side, creating a new split container. A new container will be + * created for the activity that will be started. + * @param launchingActivity An activity that should be in the primary container. If it is not + * currently in an existing container, a new one will be created and + * the activity will be re-parented to it. + * @param activityIntent The intent to start the new activity. + * @param activityOptions The options to apply to new activity start. + * @param rule The split rule to be applied to the container. + */ + void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, + @Nullable Bundle activityOptions, @NonNull SplitRule rule) { + final Rect parentBounds = getParentContainerBounds(launchingActivity); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, + isLtr(launchingActivity, rule)); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, + isLtr(launchingActivity, rule)); + + TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( + launchingActivity.getActivityToken()); + if (primaryContainer == null) { + primaryContainer = mController.newContainer(launchingActivity); + } + + TaskFragmentContainer secondaryContainer = mController.newContainer(null); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, + rule); + startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, + launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, + activityIntent, activityOptions, rule); + applyTransaction(wct); + + primaryContainer.setLastRequestedBounds(primaryRectBounds); + secondaryContainer.setLastRequestedBounds(secondaryRectBounds); + } + + /** + * Updates the positions of containers in an existing split. + * @param splitContainer The split container to be updated. + * @param updatedContainer The task fragment that was updated and caused this split update. + * @param wct WindowContainerTransaction that this update should be performed with. + */ + void updateSplitContainer(@NonNull SplitContainer splitContainer, + @NonNull TaskFragmentContainer updatedContainer, + @NonNull WindowContainerTransaction wct) { + // Getting the parent bounds using the updated container - it will have the recent value. + final Rect parentBounds = getParentContainerBounds(updatedContainer); + final SplitRule rule = splitContainer.getSplitRule(); + final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); + final Activity activity = primaryContainer.getTopNonFinishingActivity(); + if (activity == null) { + return; + } + final boolean isLtr = isLtr(activity, rule); + final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, + isLtr); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, + isLtr); + + // If the task fragments are not registered yet, the positions will be updated after they + // are created again. + resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); + + setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(), + secondaryContainer.getTaskFragmentToken(), rule); + } + + /** + * Resizes the task fragment if it was already registered. Skips the operation if the container + * creation has not been reported from the server yet. + */ + // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet. + void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container, + @Nullable Rect bounds) { + if (container.getInfo() == null) { + return; + } + resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds); + } + + @Override + void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, + @Nullable Rect bounds) { + TaskFragmentContainer container = mController.getContainer(fragmentToken); + if (container == null) { + throw new IllegalStateException( + "Resizing a task fragment that is not registered with controller."); + } + + if (container.areLastRequestedBoundsEqual(bounds)) { + // Return early if the provided bounds were already requested + return; + } + + container.setLastRequestedBounds(bounds); + super.resizeTaskFragment(wct, fragmentToken, bounds); + } + + boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) { + final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer()); + return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule()); + } + + boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) { + // TODO(b/190433398): Supply correct insets. + final WindowMetrics parentMetrics = new WindowMetrics(parentBounds, + new WindowInsets(new Rect())); + return rule.checkParentMetrics(parentMetrics); + } + + @NonNull + private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, + @NonNull SplitRule rule, boolean isLtr) { + if (!shouldShowSideBySide(parentBounds, rule)) { + return new Rect(); + } + + final float splitRatio = rule.getSplitRatio(); + final float rtlSplitRatio = 1 - splitRatio; + switch (position) { + case POSITION_START: + return isLtr ? getLeftContainerBounds(parentBounds, splitRatio) + : getRightContainerBounds(parentBounds, rtlSplitRatio); + case POSITION_END: + return isLtr ? getRightContainerBounds(parentBounds, splitRatio) + : getLeftContainerBounds(parentBounds, rtlSplitRatio); + case POSITION_FILL: + return parentBounds; + } + return parentBounds; + } + + private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) { + return new Rect( + parentBounds.left, + parentBounds.top, + (int) (parentBounds.left + parentBounds.width() * splitRatio), + parentBounds.bottom); + } + + private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) { + return new Rect( + (int) (parentBounds.left + parentBounds.width() * splitRatio), + parentBounds.top, + parentBounds.right, + parentBounds.bottom); + } + + /** + * Checks if a split with the provided rule should be displays in left-to-right layout + * direction, either always or with the current configuration. + */ + private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) { + switch (rule.getLayoutDirection()) { + case LayoutDirection.LOCALE: + return context.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; + case LayoutDirection.RTL: + return false; + case LayoutDirection.LTR: + default: + return true; + } + } + + @NonNull + Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) { + final Configuration parentConfig = mFragmentParentConfigs.get( + container.getTaskFragmentToken()); + if (parentConfig != null) { + return parentConfig.windowConfiguration.getBounds(); + } + + // If there is no parent yet - then assuming that activities are running in full task bounds + final Activity topActivity = container.getTopNonFinishingActivity(); + final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null; + + if (bounds == null) { + throw new IllegalStateException("Unknown parent bounds"); + } + return bounds; + } + + @NonNull + Rect getParentContainerBounds(@NonNull Activity activity) { + final TaskFragmentContainer container = mController.getContainerWithActivity( + activity.getActivityToken()); + if (container != null) { + final Configuration parentConfig = mFragmentParentConfigs.get( + container.getTaskFragmentToken()); + if (parentConfig != null) { + return parentConfig.windowConfiguration.getBounds(); + } + } + + // TODO(b/190433398): Check if the client-side available info about parent bounds is enough. + if (!activity.isInMultiWindowMode()) { + // In fullscreen mode the max bounds should correspond to the task bounds. + return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds(); + } + return activity.getResources().getConfiguration().windowConfiguration.getBounds(); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java new file mode 100644 index 000000000000..194b6330d92c --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2021 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 android.graphics.Matrix.MSCALE_X; + +import android.graphics.Rect; +import android.view.Choreographer; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.view.animation.Transformation; + +import androidx.annotation.NonNull; + +/** + * Wrapper to handle the TaskFragment animation update in one {@link SurfaceControl.Transaction}. + * + * The base adapter can be used for {@link RemoteAnimationTarget} that is simple open/close. + */ +class TaskFragmentAnimationAdapter { + final Animation mAnimation; + final RemoteAnimationTarget mTarget; + final SurfaceControl mLeash; + + final Transformation mTransformation = new Transformation(); + final float[] mMatrix = new float[9]; + private boolean mIsFirstFrame = true; + + TaskFragmentAnimationAdapter(@NonNull Animation animation, + @NonNull RemoteAnimationTarget target) { + this(animation, target, target.leash); + } + + /** + * @param leash the surface to animate. + */ + TaskFragmentAnimationAdapter(@NonNull Animation animation, + @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash) { + mAnimation = animation; + mTarget = target; + mLeash = leash; + } + + /** Called on frame update. */ + final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { + if (mIsFirstFrame) { + t.show(mLeash); + mIsFirstFrame = false; + } + + // Extract the transformation to the current time. + mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()), + mTransformation); + t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + onAnimationUpdateInner(t); + } + + /** To be overridden by subclasses to adjust the animation surface change. */ + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + mTransformation.getMatrix().postTranslate( + mTarget.localBounds.left, mTarget.localBounds.top); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + } + + /** Called after animation finished. */ + final void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { + onAnimationUpdate(t, mAnimation.getDuration()); + } + + final long getDurationHint() { + return mAnimation.computeDurationHint(); + } + + /** + * Should be used when the {@link RemoteAnimationTarget} is in split with others, and want to + * animate together as one. This adapter will offset the animation leash to make the animate of + * two windows look like a single window. + */ + static class SplitAdapter extends TaskFragmentAnimationAdapter { + private final boolean mIsLeftHalf; + private final int mWholeAnimationWidth; + + /** + * @param isLeftHalf whether this is the left half of the animation. + * @param wholeAnimationWidth the whole animation windows width. + */ + SplitAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target, + boolean isLeftHalf, int wholeAnimationWidth) { + super(animation, target); + mIsLeftHalf = isLeftHalf; + mWholeAnimationWidth = wholeAnimationWidth; + if (wholeAnimationWidth == 0) { + throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth"); + } + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + float posX = mTarget.localBounds.left; + final float posY = mTarget.localBounds.top; + // This window is half of the whole animation window. Offset left/right to make it + // look as one with the other half. + mTransformation.getMatrix().getValues(mMatrix); + final int targetWidth = mTarget.localBounds.width(); + final float scaleX = mMatrix[MSCALE_X]; + final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2; + final float curOffset = targetWidth * (1 - scaleX) / 2; + final float offsetDiff = totalOffset - curOffset; + if (mIsLeftHalf) { + posX += offsetDiff; + } else { + posX -= offsetDiff; + } + mTransformation.getMatrix().postTranslate(posX, posY); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + } + } + + /** + * Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has + * size change. + */ + static class SnapshotAdapter extends TaskFragmentAnimationAdapter { + + SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { + // Start leash is the snapshot of the starting surface. + super(animation, target, target.startLeash); + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + // Snapshot should always be placed at the top left of the animation leash. + mTransformation.getMatrix().postTranslate(0, 0); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + } + } + + /** + * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change. + */ + static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter { + private final float[] mVecs = new float[4]; + private final Rect mRect = new Rect(); + + BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { + super(animation, target); + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + mTransformation.getMatrix().postTranslate( + mTarget.localBounds.left, mTarget.localBounds.top); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); + + // The following applies an inverse scale to the clip-rect so that it crops "after" the + // scale instead of before. + mVecs[1] = mVecs[2] = 0; + mVecs[0] = mVecs[3] = 1; + mTransformation.getMatrix().mapVectors(mVecs); + mVecs[0] = 1.f / mVecs[0]; + mVecs[3] = 1.f / mVecs[3]; + final Rect clipRect = mTransformation.getClipRect(); + mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); + mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); + mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); + mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); + t.setWindowCrop(mLeash, mRect); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java new file mode 100644 index 000000000000..535dac1a5101 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 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 android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; + +import android.util.Log; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationDefinition; +import android.window.TaskFragmentOrganizer; + +/** Controls the TaskFragment remote animations. */ +class TaskFragmentAnimationController { + + private static final String TAG = "TaskFragAnimationCtrl"; + static final boolean DEBUG = false; + + private final TaskFragmentOrganizer mOrganizer; + private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner(); + + TaskFragmentAnimationController(TaskFragmentOrganizer organizer) { + mOrganizer = organizer; + } + + void registerRemoteAnimations() { + if (DEBUG) { + Log.v(TAG, "registerRemoteAnimations"); + } + final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); + final RemoteAnimationAdapter animationAdapter = + new RemoteAnimationAdapter(mRemoteRunner, 0, 0, true /* changeNeedsSnapshot */); + definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, animationAdapter); + definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, animationAdapter); + definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter); + mOrganizer.registerRemoteAnimations(definition); + } + + void unregisterRemoteAnimations() { + if (DEBUG) { + Log.v(TAG, "unregisterRemoteAnimations"); + } + mOrganizer.unregisterRemoteAnimations(); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java new file mode 100644 index 000000000000..412559e34070 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2021 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 android.view.RemoteAnimationTarget.MODE_CLOSING; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; +import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.view.animation.Animation; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +/** To run the TaskFragment animations. */ +class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { + + private static final String TAG = "TaskFragAnimationRunner"; + private final Handler mHandler = new Handler(Looper.myLooper()); + private final TaskFragmentAnimationSpec mAnimationSpec; + + TaskFragmentAnimationRunner() { + mAnimationSpec = new TaskFragmentAnimationSpec(mHandler); + } + + @Nullable + private Animator mAnimator; + + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + @NonNull RemoteAnimationTarget[] apps, + @NonNull RemoteAnimationTarget[] wallpapers, + @NonNull RemoteAnimationTarget[] nonApps, + @NonNull IRemoteAnimationFinishedCallback finishedCallback) { + if (wallpapers.length != 0 || nonApps.length != 0) { + throw new IllegalArgumentException("TaskFragment shouldn't handle animation with" + + "wallpaper or non-app windows."); + } + if (TaskFragmentAnimationController.DEBUG) { + Log.v(TAG, "onAnimationStart transit=" + transit); + } + mHandler.post(() -> startAnimation(transit, apps, finishedCallback)); + } + + @Override + public void onAnimationCancelled() { + if (TaskFragmentAnimationController.DEBUG) { + Log.v(TAG, "onAnimationCancelled"); + } + mHandler.post(this::cancelAnimation); + } + + /** Creates and starts animation. */ + private void startAnimation(@WindowManager.TransitionOldType int transit, + @NonNull RemoteAnimationTarget[] targets, + @NonNull IRemoteAnimationFinishedCallback finishedCallback) { + if (mAnimator != null) { + Log.w(TAG, "start new animation when the previous one is not finished yet."); + mAnimator.cancel(); + } + mAnimator = createAnimator(transit, targets, finishedCallback); + mAnimator.start(); + } + + /** Cancels animation. */ + private void cancelAnimation() { + if (mAnimator == null) { + return; + } + mAnimator.cancel(); + mAnimator = null; + } + + /** Creates the animator given the transition type and windows. */ + private Animator createAnimator(@WindowManager.TransitionOldType int transit, + @NonNull RemoteAnimationTarget[] targets, + @NonNull IRemoteAnimationFinishedCallback finishedCallback) { + final List<TaskFragmentAnimationAdapter> adapters = + createAnimationAdapters(transit, targets); + long duration = 0; + for (TaskFragmentAnimationAdapter adapter : adapters) { + duration = Math.max(duration, adapter.getDurationHint()); + } + final ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setDuration(duration); + animator.addUpdateListener((anim) -> { + // Update all adapters in the same transaction. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (TaskFragmentAnimationAdapter adapter : adapters) { + adapter.onAnimationUpdate(t, animator.getCurrentPlayTime()); + } + t.apply(); + }); + animator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationEnd(Animator animation) { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (TaskFragmentAnimationAdapter adapter : adapters) { + adapter.onAnimationEnd(t); + } + t.apply(); + + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + mAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + return animator; + } + + /** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */ + private List<TaskFragmentAnimationAdapter> createAnimationAdapters( + @WindowManager.TransitionOldType int transit, + @NonNull RemoteAnimationTarget[] targets) { + switch (transit) { + case TRANSIT_OLD_TASK_FRAGMENT_OPEN: + return createOpenAnimationAdapters(targets); + case TRANSIT_OLD_TASK_FRAGMENT_CLOSE: + return createCloseAnimationAdapters(targets); + case TRANSIT_OLD_TASK_FRAGMENT_CHANGE: + return createChangeAnimationAdapters(targets); + default: + throw new IllegalArgumentException("Unhandled transit type=" + transit); + } + } + + private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters( + @NonNull RemoteAnimationTarget[] targets) { + return createOpenCloseAnimationAdapters(targets, + mAnimationSpec::loadOpenAnimation); + } + + private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters( + @NonNull RemoteAnimationTarget[] targets) { + return createOpenCloseAnimationAdapters(targets, + mAnimationSpec::loadCloseAnimation); + } + + private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters( + @NonNull RemoteAnimationTarget[] targets, + @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) { + // We need to know if the target window is only a partial of the whole animation screen. + // If so, we will need to adjust it to make the whole animation screen looks like one. + final List<RemoteAnimationTarget> openingTargets = new ArrayList<>(); + final List<RemoteAnimationTarget> closingTargets = new ArrayList<>(); + final Rect openingWholeScreenBounds = new Rect(); + final Rect closingWholeScreenBounds = new Rect(); + for (RemoteAnimationTarget target : targets) { + if (target.mode != MODE_CLOSING) { + openingTargets.add(target); + openingWholeScreenBounds.union(target.localBounds); + } else { + closingTargets.add(target); + closingWholeScreenBounds.union(target.localBounds); + } + } + + final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>(); + for (RemoteAnimationTarget target : openingTargets) { + adapters.add(createOpenCloseAnimationAdapter(target, animationProvider, + openingWholeScreenBounds)); + } + for (RemoteAnimationTarget target : closingTargets) { + adapters.add(createOpenCloseAnimationAdapter(target, animationProvider, + closingWholeScreenBounds)); + } + return adapters; + } + + private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter( + @NonNull RemoteAnimationTarget target, + @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider, + @NonNull Rect wholeAnimationBounds) { + final Animation animation = animationProvider.apply(target, wholeAnimationBounds); + final Rect targetBounds = target.localBounds; + if (targetBounds.left == wholeAnimationBounds.left + && targetBounds.right != wholeAnimationBounds.right) { + // This is the left split of the whole animation window. + return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target, + true /* isLeftHalf */, wholeAnimationBounds.width()); + } else if (targetBounds.left != wholeAnimationBounds.left + && targetBounds.right == wholeAnimationBounds.right) { + // This is the right split of the whole animation window. + return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target, + false /* isLeftHalf */, wholeAnimationBounds.width()); + } + // Open/close window that fills the whole animation. + return new TaskFragmentAnimationAdapter(animation, target); + } + + private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters( + @NonNull RemoteAnimationTarget[] targets) { + final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>(); + for (RemoteAnimationTarget target : targets) { + if (target.startBounds != null) { + // This is the target with bounds change. + final Animation[] animations = + mAnimationSpec.createChangeBoundsChangeAnimations(target); + // Adapter for the starting snapshot leash. + adapters.add(new TaskFragmentAnimationAdapter.SnapshotAdapter( + animations[0], target)); + // Adapter for the ending bounds changed leash. + adapters.add(new TaskFragmentAnimationAdapter.BoundsChangeAdapter( + animations[1], target)); + continue; + } + + // These are the other targets that don't have bounds change in the same transition. + final Animation animation; + if (target.hasAnimatingParent) { + // No-op if it will be covered by the changing parent window. + animation = TaskFragmentAnimationSpec.createNoopAnimation(target); + } else if (target.mode == MODE_CLOSING) { + animation = mAnimationSpec.createChangeBoundsCloseAnimation(target); + } else { + animation = mAnimationSpec.createChangeBoundsOpenAnimation(target); + } + adapters.add(new TaskFragmentAnimationAdapter(animation, target)); + } + return adapters; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java new file mode 100644 index 000000000000..c0908a548501 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2021 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 android.view.RemoteAnimationTarget.MODE_CLOSING; + +import android.app.ActivityThread; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.graphics.Rect; +import android.os.Handler; +import android.provider.Settings; +import android.view.RemoteAnimationTarget; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.ClipRectAnimation; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.TranslateAnimation; + +import androidx.annotation.NonNull; + +import com.android.internal.R; +import com.android.internal.policy.AttributeCache; +import com.android.internal.policy.TransitionAnimation; + +/** Animation spec for TaskFragment transition. */ +class TaskFragmentAnimationSpec { + + private static final String TAG = "TaskFragAnimationSpec"; + private static final int CHANGE_ANIMATION_DURATION = 517; + private static final int CHANGE_ANIMATION_FADE_DURATION = 80; + private static final int CHANGE_ANIMATION_FADE_OFFSET = 30; + + private final Context mContext; + private final TransitionAnimation mTransitionAnimation; + private final Interpolator mFastOutExtraSlowInInterpolator; + private final LinearInterpolator mLinearInterpolator; + private float mTransitionAnimationScaleSetting; + + TaskFragmentAnimationSpec(@NonNull Handler handler) { + mContext = ActivityThread.currentActivityThread().getApplication(); + mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG); + // Initialize the AttributeCache for the TransitionAnimation. + AttributeCache.init(mContext); + mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_extra_slow_in); + mLinearInterpolator = new LinearInterpolator(); + + // The transition animation should be adjusted based on the developer option. + final ContentResolver resolver = mContext.getContentResolver(); + mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver, + Settings.Global.TRANSITION_ANIMATION_SCALE, + mContext.getResources().getFloat( + R.dimen.config_appTransitionAnimationDurationScaleDefault)); + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false, + new SettingsObserver(handler)); + } + + /** For target that doesn't need to be animated. */ + static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) { + // Noop but just keep the target showing/hiding. + final float alpha = target.mode == MODE_CLOSING ? 0f : 1f; + return new AlphaAnimation(alpha, alpha); + } + + /** Animation for target that is opening in a change transition. */ + Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) { + final Rect bounds = target.localBounds; + // The target will be animated in from left or right depends on its position. + final int startLeft = bounds.left == 0 ? -bounds.width() : bounds.width(); + + // The position should be 0-based as we will post translate in + // TaskFragmentAnimationAdapter#onAnimationUpdate + final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0); + animation.setInterpolator(mFastOutExtraSlowInInterpolator); + animation.setDuration(CHANGE_ANIMATION_DURATION); + animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + /** Animation for target that is closing in a change transition. */ + Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) { + final Rect bounds = target.localBounds; + // The target will be animated out to left or right depends on its position. + final int endLeft = bounds.left == 0 ? -bounds.width() : bounds.width(); + + // The position should be 0-based as we will post translate in + // TaskFragmentAnimationAdapter#onAnimationUpdate + final Animation animation = new TranslateAnimation(0, endLeft, 0, 0); + animation.setInterpolator(mFastOutExtraSlowInInterpolator); + animation.setDuration(CHANGE_ANIMATION_DURATION); + animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + /** + * Animation for target that is changing (bounds change) in a change transition. + * @return the return array always has two elements. The first one is for the start leash, and + * the second one is for the end leash. + */ + Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) { + // Both start bounds and end bounds are in screen coordinates. We will post translate + // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate + final Rect startBounds = target.startBounds; + final Rect parentBounds = target.taskInfo.configuration.windowConfiguration.getBounds(); + final Rect endBounds = target.screenSpaceBounds; + float scaleX = ((float) startBounds.width()) / endBounds.width(); + float scaleY = ((float) startBounds.height()) / endBounds.height(); + // Start leash is a child of the end leash. Reverse the scale so that the start leash won't + // be scaled up with its parent. + float startScaleX = 1.f / scaleX; + float startScaleY = 1.f / scaleY; + + // The start leash will be fade out. + final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */); + final Animation startAlpha = new AlphaAnimation(1f, 0f); + startAlpha.setInterpolator(mLinearInterpolator); + startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION); + startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET); + startSet.addAnimation(startAlpha); + final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY, + startScaleY); + startScale.setInterpolator(mFastOutExtraSlowInInterpolator); + startScale.setDuration(CHANGE_ANIMATION_DURATION); + startSet.addAnimation(startScale); + startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(), + endBounds.height()); + startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); + + // The end leash will be moved into the end position while scaling. + final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */); + endSet.setInterpolator(mFastOutExtraSlowInInterpolator); + final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1); + endScale.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(endScale); + // The position should be 0-based as we will post translate in + // TaskFragmentAnimationAdapter#onAnimationUpdate + final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0, + 0, 0); + endTranslate.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(endTranslate); + // The end leash is resizing, we should update the window crop based on the clip rect. + final Rect startClip = new Rect(startBounds); + final Rect endClip = new Rect(endBounds); + startClip.offsetTo(0, 0); + endClip.offsetTo(0, 0); + final Animation clipAnim = new ClipRectAnimation(startClip, endClip); + clipAnim.setDuration(CHANGE_ANIMATION_DURATION); + endSet.addAnimation(clipAnim); + endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(), + parentBounds.height()); + endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); + + return new Animation[]{startSet, endSet}; + } + + Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target, + @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = target.mode != MODE_CLOSING; + final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter + ? R.styleable.WindowAnimation_activityOpenEnterAnimation + : R.styleable.WindowAnimation_activityOpenExitAnimation); + animation.initialize(target.localBounds.width(), target.localBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target, + @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = target.mode != MODE_CLOSING; + final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation); + animation.initialize(target.localBounds.width(), target.localBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; + } + + private class SettingsObserver extends ContentObserver { + SettingsObserver(@NonNull Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + mTransitionAnimationScaleSetting = Settings.Global.getFloat( + mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE, + mTransitionAnimationScaleSetting); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java new file mode 100644 index 000000000000..54e44a70ed40 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2021 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.ActivityThread; +import android.graphics.Rect; +import android.os.Binder; +import android.os.IBinder; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerTransaction; + +import java.util.ArrayList; +import java.util.List; + +/** + * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment + * on the server side. + */ +class TaskFragmentContainer { + /** + * Client-created token that uniquely identifies the task fragment container instance. + */ + @NonNull + private final IBinder mToken; + + /** + * Server-provided task fragment information. + */ + private TaskFragmentInfo mInfo; + + /** + * Activities that are being reparented or being started to this container, but haven't been + * added to {@link #mInfo} yet. + */ + private final ArrayList<Activity> mPendingAppearedActivities = new ArrayList<>(); + + /** Containers that are dependent on this one and should be completely destroyed on exit. */ + private final List<TaskFragmentContainer> mContainersToFinishOnExit = + new ArrayList<>(); + + /** Individual associated activities in different containers that should be finished on exit. */ + private final List<Activity> mActivitiesToFinishOnExit = new ArrayList<>(); + + /** Indicates whether the container was cleaned up after the last activity was removed. */ + private boolean mIsFinished; + + /** + * Bounds that were requested last via {@link android.window.WindowContainerTransaction}. + */ + private final Rect mLastRequestedBounds = new Rect(); + + /** + * Creates a container with an existing activity that will be re-parented to it in a window + * container transaction. + */ + TaskFragmentContainer(@Nullable Activity activity) { + mToken = new Binder("TaskFragmentContainer"); + if (activity != null) { + addPendingAppearedActivity(activity); + } + } + + /** + * Returns the client-created token that uniquely identifies this container. + */ + @NonNull + IBinder getTaskFragmentToken() { + return mToken; + } + + /** List of activities that belong to this container and live in this process. */ + @NonNull + List<Activity> collectActivities() { + // Add the re-parenting activity, in case the server has not yet reported the task + // fragment info update with it placed in this container. We still want to apply rules + // in this intermediate state. + List<Activity> allActivities = new ArrayList<>(); + if (!mPendingAppearedActivities.isEmpty()) { + allActivities.addAll(mPendingAppearedActivities); + } + // Add activities reported from the server. + if (mInfo == null) { + return allActivities; + } + ActivityThread activityThread = ActivityThread.currentActivityThread(); + for (IBinder token : mInfo.getActivities()) { + Activity activity = activityThread.getActivity(token); + if (activity != null && !allActivities.contains(activity)) { + allActivities.add(activity); + } + } + return allActivities; + } + + void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { + mPendingAppearedActivities.add(pendingAppearedActivity); + } + + boolean hasActivity(@NonNull IBinder token) { + if (mInfo != null && mInfo.getActivities().contains(token)) { + return true; + } + for (Activity activity : mPendingAppearedActivities) { + if (activity.getActivityToken().equals(token)) { + return true; + } + } + return false; + } + + int getRunningActivityCount() { + int count = mPendingAppearedActivities.size(); + if (mInfo != null) { + count += mInfo.getRunningActivityCount(); + } + return count; + } + + @Nullable + TaskFragmentInfo getInfo() { + return mInfo; + } + + void setInfo(@NonNull TaskFragmentInfo info) { + mInfo = info; + if (mInfo == null || mPendingAppearedActivities.isEmpty()) { + return; + } + // Cleanup activities that were being re-parented + List<IBinder> infoActivities = mInfo.getActivities(); + for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) { + final Activity activity = mPendingAppearedActivities.get(i); + if (infoActivities.contains(activity.getActivityToken())) { + mPendingAppearedActivities.remove(i); + } + } + } + + @Nullable + Activity getTopNonFinishingActivity() { + List<Activity> activities = collectActivities(); + if (activities.isEmpty()) { + return null; + } + int i = activities.size() - 1; + while (i >= 0 && activities.get(i).isFinishing()) { + i--; + } + return i >= 0 ? activities.get(i) : null; + } + + boolean isEmpty() { + return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty()); + } + + /** + * Adds a container that should be finished when this container is finished. + */ + void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) { + mContainersToFinishOnExit.add(containerToFinish); + } + + /** + * Adds an activity that should be finished when this container is finished. + */ + void addActivityToFinishOnExit(@NonNull Activity activityToFinish) { + mActivitiesToFinishOnExit.add(activityToFinish); + } + + /** + * Removes all activities that belong to this process and finishes other containers/activities + * configured to finish together. + */ + void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, + @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { + if (!mIsFinished) { + mIsFinished = true; + finishActivities(shouldFinishDependent, presenter, wct, controller); + } + + if (mInfo == null) { + // Defer removal the container and wait until TaskFragment appeared. + return; + } + + // Cleanup the visuals + presenter.deleteTaskFragment(wct, getTaskFragmentToken()); + // Cleanup the records + controller.removeContainer(this); + // Clean up task fragment information + mInfo = null; + } + + private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, + @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { + // Finish own activities + for (Activity activity : collectActivities()) { + if (!activity.isFinishing()) { + activity.finish(); + } + } + + if (!shouldFinishDependent) { + return; + } + + // Finish dependent containers + for (TaskFragmentContainer container : mContainersToFinishOnExit) { + container.finish(true /* shouldFinishDependent */, presenter, + wct, controller); + } + mContainersToFinishOnExit.clear(); + + // Finish associated activities + for (Activity activity : mActivitiesToFinishOnExit) { + activity.finish(); + } + mActivitiesToFinishOnExit.clear(); + + // Finish activities that were being re-parented to this container. + for (Activity activity : mPendingAppearedActivities) { + activity.finish(); + } + mPendingAppearedActivities.clear(); + } + + boolean isFinished() { + return mIsFinished; + } + + /** + * Checks if last requested bounds are equal to the provided value. + */ + boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) { + return (bounds == null && mLastRequestedBounds.isEmpty()) + || mLastRequestedBounds.equals(bounds); + } + + /** + * Updates the last requested bounds. + */ + void setLastRequestedBounds(@Nullable Rect bounds) { + if (bounds == null) { + mLastRequestedBounds.setEmpty(); + } else { + mLastRequestedBounds.set(bounds); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index a0d5b004ff1c..383d91da6af8 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.window.extensions; +package androidx.window.extensions.layout; import static android.view.Display.DEFAULT_DISPLAY; @@ -36,19 +36,27 @@ import androidx.window.util.DataProducer; import androidx.window.util.PriorityDataProducer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; /** - * Reference implementation of androidx.window.extensions OEM interface for use with + * Reference implementation of androidx.window.extensions.layout OEM interface for use with * WindowManager Jetpack. * * NOTE: This version is a work in progress and under active development. It MUST NOT be used in * production builds since the interface can still change before reaching stable version. * Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead. */ -class SampleExtensionImpl extends StubExtension { +public class WindowLayoutComponentImpl implements WindowLayoutComponent { private static final String TAG = "SampleExtension"; + private static WindowLayoutComponent sInstance; + + private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = + new HashMap<>(); private final SettingsDevicePostureProducer mSettingsDevicePostureProducer; private final DataProducer<Integer> mDevicePostureProducer; @@ -56,7 +64,7 @@ class SampleExtensionImpl extends StubExtension { private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer; private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer; - SampleExtensionImpl(Context context) { + public WindowLayoutComponentImpl(Context context) { mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context); mDevicePostureProducer = new PriorityDataProducer<>(List.of( mSettingsDevicePostureProducer, @@ -73,28 +81,68 @@ class SampleExtensionImpl extends StubExtension { mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } + /** + * Adds a listener interested in receiving updates to {@link WindowLayoutInfo} + * @param activity hosting a {@link android.view.Window} + * @param consumer interested in receiving updates to {@link WindowLayoutInfo} + */ + public void addWindowLayoutInfoListener(@NonNull Activity activity, + @NonNull Consumer<WindowLayoutInfo> consumer) { + mWindowLayoutChangeListeners.put(activity, consumer); + updateRegistrations(); + } + + /** + * Removes a listener no longer interested in receiving updates. + * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} + */ + public void removeWindowLayoutInfoListener( + @NonNull Consumer<WindowLayoutInfo> consumer) { + mWindowLayoutChangeListeners.values().remove(consumer); + updateRegistrations(); + } + + void updateWindowLayout(@NonNull Activity activity, + @NonNull WindowLayoutInfo newLayout) { + Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity); + if (consumer != null) { + consumer.accept(newLayout); + } + } + + @NonNull + Set<Activity> getActivitiesListeningForLayoutChanges() { + return mWindowLayoutChangeListeners.keySet(); + } + + protected boolean hasListeners() { + return !mWindowLayoutChangeListeners.isEmpty(); + } + private int getFeatureState(DisplayFeature feature) { Integer featureState = feature.getState(); Optional<Integer> posture = mDevicePostureProducer.getData(); - int fallbackPosture = posture.orElse(ExtensionFoldingFeature.STATE_FLAT); + int fallbackPosture = posture.orElse(FoldingFeature.STATE_FLAT); return featureState == null ? fallbackPosture : featureState; } private void onDisplayFeaturesChanged() { for (Activity activity : getActivitiesListeningForLayoutChanges()) { - ExtensionWindowLayoutInfo newLayout = getWindowLayoutInfo(activity); + WindowLayoutInfo newLayout = getWindowLayoutInfo(activity); updateWindowLayout(activity, newLayout); } } @NonNull - private ExtensionWindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { - List<ExtensionDisplayFeature> displayFeatures = getDisplayFeatures(activity); - return new ExtensionWindowLayoutInfo(displayFeatures); + private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { + List<androidx.window.extensions.layout.DisplayFeature> displayFeatures = + getDisplayFeatures(activity); + return new WindowLayoutInfo(displayFeatures); } - private List<ExtensionDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { - List<ExtensionDisplayFeature> features = new ArrayList<>(); + private List<androidx.window.extensions.layout.DisplayFeature> getDisplayFeatures( + @NonNull Activity activity) { + List<androidx.window.extensions.layout.DisplayFeature> features = new ArrayList<>(); int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { Log.w(TAG, "This sample doesn't support display features on secondary displays"); @@ -115,15 +163,14 @@ class SampleExtensionImpl extends StubExtension { rotateRectToDisplayRotation(displayId, featureRect); transformToWindowSpaceRect(activity, featureRect); - features.add(new ExtensionFoldingFeature(featureRect, baseFeature.getType(), + features.add(new FoldingFeature(featureRect, baseFeature.getType(), getFeatureState(baseFeature))); } } return features; } - @Override - protected void onListenersChanged() { + private void updateRegistrations() { if (hasListeners()) { mSettingsDevicePostureProducer.registerObserversIfNeeded(); mSettingsDisplayFeatureProducer.registerObserversIfNeeded(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java index e6f8388b031f..62959b7b95e9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java @@ -28,7 +28,7 @@ public class SidecarProvider { * an OEM by overriding this method. */ public static SidecarInterface getSidecarImpl(Context context) { - return new SampleSidecarImpl(context); + return new SampleSidecarImpl(context.getApplicationContext()); } /** @@ -36,6 +36,6 @@ public class SidecarProvider { * @return API version string in MAJOR.MINOR.PATCH-description format. */ public static String getApiVersion() { - return "0.1.0-settings_sample"; + return "1.0.0-reference"; } } diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex be6652d43fb2..4f36c9c690c9 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 9aaef3b1f655..3ba1a34bd432 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -39,6 +39,14 @@ filegroup { } filegroup { + name: "wm_shell_util-sources", + srcs: [ + "src/com/android/wm/shell/util/**/*.java", + ], + path: "src", +} + +filegroup { name: "wm_shell-aidls", srcs: [ "src/**/*.aidl", diff --git a/libs/WindowManager/Shell/res/color/split_divider_background.xml b/libs/WindowManager/Shell/res/color/split_divider_background.xml new file mode 100644 index 000000000000..329e5b9b31a0 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/split_divider_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_500" android:lStar="35" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml b/libs/WindowManager/Shell/res/color/unfold_transition_background.xml new file mode 100644 index 000000000000..63289a3f75d9 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/unfold_transition_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Matches taskbar color --> + <item android:color="@android:color/system_neutral2_500" android:lStar="35" /> +</selector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml index 8710fb8ac69b..96d2d7c954d8 100644 --- a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml +++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml @@ -18,7 +18,7 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid - android:color="@android:color/system_neutral1_900" + android:color="@android:color/system_neutral1_800" /> <corners android:radius="20dp" /> diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml index c09ae53746da..0cf6d73162d2 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml @@ -17,13 +17,13 @@ <com.android.wm.shell.common.AlphaOptimizedButton xmlns:android="http://schemas.android.com/apk/res/android" style="@android:style/Widget.DeviceDefault.Button.Borderless" - android:id="@+id/settings_button" + android:id="@+id/manage_button" android:layout_gravity="start" android:layout_width="wrap_content" - android:layout_height="40dp" - android:layout_marginTop="8dp" - android:layout_marginLeft="16dp" - android:layout_marginBottom="8dp" + android:layout_height="@dimen/bubble_manage_button_height" + android:layout_marginStart="@dimen/bubble_manage_button_margin" + android:layout_marginTop="@dimen/bubble_manage_button_margin" + android:layout_marginBottom="@dimen/bubble_manage_button_margin" android:focusable="true" android:text="@string/manage_bubbles_text" android:textSize="@*android:dimen/text_size_body_2_material" diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml index f4b3aca33dd7..298ad3025b00 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml @@ -25,15 +25,15 @@ android:id="@+id/bubble_manage_menu_dismiss_container" android:background="@drawable/bubble_manage_menu_row" android:layout_width="match_parent" - android:layout_height="48dp" + android:layout_height="@dimen/bubble_menu_item_height" android:gravity="center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/bubble_menu_padding" + android:paddingEnd="@dimen/bubble_menu_padding" android:orientation="horizontal"> <ImageView - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/bubble_menu_icon_size" + android:layout_height="@dimen/bubble_menu_icon_size" android:src="@drawable/ic_remove_no_shadow" android:tint="@color/bubbles_icon_tint"/> @@ -50,15 +50,15 @@ android:id="@+id/bubble_manage_menu_dont_bubble_container" android:background="@drawable/bubble_manage_menu_row" android:layout_width="match_parent" - android:layout_height="48dp" + android:layout_height="@dimen/bubble_menu_item_height" android:gravity="center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/bubble_menu_padding" + android:paddingEnd="@dimen/bubble_menu_padding" android:orientation="horizontal"> <ImageView - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/bubble_menu_icon_size" + android:layout_height="@dimen/bubble_menu_icon_size" android:src="@drawable/bubble_ic_stop_bubble" android:tint="@color/bubbles_icon_tint"/> @@ -75,16 +75,16 @@ android:id="@+id/bubble_manage_menu_settings_container" android:background="@drawable/bubble_manage_menu_row" android:layout_width="match_parent" - android:layout_height="48dp" + android:layout_height="@dimen/bubble_menu_item_height" android:gravity="center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/bubble_menu_padding" + android:paddingEnd="@dimen/bubble_menu_padding" android:orientation="horizontal"> <ImageView android:id="@+id/bubble_manage_menu_settings_icon" - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/bubble_menu_icon_size" + android:layout_height="@dimen/bubble_menu_icon_size" android:src="@drawable/ic_remove_no_shadow"/> <TextView diff --git a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml index fd4c3ba87026..87deb8b5a1fd 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml @@ -21,7 +21,6 @@ android:layout_width="wrap_content" android:paddingTop="48dp" android:paddingBottom="48dp" - android:paddingStart="@dimen/bubble_stack_user_education_side_inset" android:paddingEnd="16dp" android:layout_marginEnd="24dp" android:orientation="vertical" diff --git a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml index c5c42fca323d..fafe40e924f5 100644 --- a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml @@ -23,7 +23,6 @@ android:clickable="true" android:paddingTop="28dp" android:paddingBottom="16dp" - android:paddingStart="@dimen/bubble_expanded_view_padding" android:paddingEnd="48dp" android:layout_marginEnd="24dp" android:orientation="vertical" @@ -66,27 +65,21 @@ android:id="@+id/button_layout" android:orientation="horizontal" > - <com.android.wm.shell.common.AlphaOptimizedButton - style="@android:style/Widget.Material.Button.Borderless" - android:id="@+id/manage" - android:layout_gravity="start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:focusable="true" - android:clickable="false" - android:text="@string/manage_bubbles_text" - android:textColor="@android:color/system_neutral1_900" + <include + layout="@layout/bubble_manage_button" /> <com.android.wm.shell.common.AlphaOptimizedButton - style="@android:style/Widget.Material.Button.Borderless" + style="@android:style/Widget.DeviceDefault.Button.Borderless" android:id="@+id/got_it" android:layout_gravity="start" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="@dimen/bubble_manage_button_height" android:focusable="true" android:text="@string/bubbles_user_education_got_it" + android:textSize="@*android:dimen/text_size_body_2_material" android:textColor="@android:color/system_neutral1_900" + android:background="@drawable/bubble_manage_btn_bg" /> </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml index ed5d2e1b49f5..d732b01ce106 100644 --- a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml +++ b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml @@ -22,7 +22,7 @@ <View style="@style/DockedDividerBackground" android:id="@+id/docked_divider_background" - android:background="@color/docked_divider_background"/> + android:background="@color/split_divider_background"/> <com.android.wm.shell.legacysplitscreen.MinimizedDockShadow style="@style/DockedDividerMinimizedShadow" diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml index 7f583f3e6bac..e3be700469a7 100644 --- a/libs/WindowManager/Shell/res/layout/split_divider.xml +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -19,15 +19,25 @@ android:layout_height="match_parent" android:layout_width="match_parent"> - <View - style="@style/DockedDividerBackground" - android:id="@+id/docked_divider_background" - android:background="@color/docked_divider_background"/> + <FrameLayout + android:id="@+id/divider_bar" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <com.android.wm.shell.common.split.DividerHandleView - style="@style/DockedDividerHandle" - android:id="@+id/docked_divider_handle" - android:contentDescription="@string/accessibility_divider" - android:background="@null"/> + <View + style="@style/DockedDividerBackground" + android:id="@+id/docked_divider_background"/> + + <com.android.wm.shell.common.split.DividerHandleView + style="@style/DockedDividerHandle" + android:id="@+id/docked_divider_handle" + android:contentDescription="@string/accessibility_divider" + android:background="@null"/> + + <com.android.wm.shell.common.split.DividerRoundedCorner + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + </FrameLayout> </com.android.wm.shell.common.split.DividerView> diff --git a/libs/WindowManager/Shell/res/layout/split_outline.xml b/libs/WindowManager/Shell/res/layout/split_outline.xml new file mode 100644 index 000000000000..13a30f5a0423 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/split_outline.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 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. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.wm.shell.splitscreen.OutlineView + android:id="@+id/split_outline" + android:layout_height="match_parent" + android:layout_width="match_parent" /> + +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml index aafba58cef59..a95323fd4801 100644 --- a/libs/WindowManager/Shell/res/values-land/dimens.xml +++ b/libs/WindowManager/Shell/res/values-land/dimens.xml @@ -16,8 +16,12 @@ */ --> <resources> + <!-- Divider handle size for legacy split screen --> <dimen name="docked_divider_handle_width">2dp</dimen> <dimen name="docked_divider_handle_height">16dp</dimen> + <!-- Divider handle size for split screen --> + <dimen name="split_divider_handle_width">3dp</dimen> + <dimen name="split_divider_handle_height">72dp</dimen> <!-- Padding between status bar and bubbles when displayed in expanded state, smaller value in landscape since we have limited vertical space--> diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml index 863bb69d4034..9eddac48e6de 100644 --- a/libs/WindowManager/Shell/res/values-land/styles.xml +++ b/libs/WindowManager/Shell/res/values-land/styles.xml @@ -19,6 +19,7 @@ <item name="android:layout_width">10dp</item> <item name="android:layout_height">match_parent</item> <item name="android:layout_gravity">center_horizontal</item> + <item name="android:background">@color/split_divider_background</item> </style> <style name="DockedDividerHandle"> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 350beafae961..93c0352a2ad3 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -17,7 +17,6 @@ */ --> <resources> - <color name="docked_divider_background">#ff000000</color> <color name="docked_divider_handle">#ffffff</color> <drawable name="forced_resizable_background">#59000000</drawable> <color name="minimize_dock_shadow_start">#60000000</color> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f28ee820eb35..f85766437b44 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -76,8 +76,15 @@ <!-- How high we lift the divider when touching --> <dimen name="docked_stack_divider_lift_elevation">4dp</dimen> + <!-- Divider handle size for legacy split screen --> <dimen name="docked_divider_handle_width">16dp</dimen> <dimen name="docked_divider_handle_height">2dp</dimen> + <!-- Divider handle size for split screen --> + <dimen name="split_divider_handle_width">72dp</dimen> + <dimen name="split_divider_handle_height">3dp</dimen> + + <dimen name="split_divider_bar_width">10dp</dimen> + <dimen name="split_divider_corner_size">42dp</dimen> <!-- One-Handed Mode --> <!-- Threshold for dragging distance to enable one-handed mode --> @@ -100,6 +107,8 @@ <dimen name="bubble_flyout_space_from_bubble">8dp</dimen> <!-- How much space to leave between the flyout text and the avatar displayed in the flyout. --> <dimen name="bubble_flyout_avatar_message_space">6dp</dimen> + <!-- If the screen percentage is smaller than this, we'll use this value instead. --> + <dimen name="bubbles_flyout_min_width_large_screen">200dp</dimen> <!-- Padding between status bar and bubbles when displayed in expanded state --> <dimen name="bubble_padding_top">16dp</dimen> <!-- Space between bubbles when expanded. --> @@ -122,7 +131,7 @@ should also be updated. --> <dimen name="bubble_expanded_default_height">180dp</dimen> <!-- On large screens the width of the expanded view is restricted to this size. --> - <dimen name="bubble_expanded_view_tablet_width">412dp</dimen> + <dimen name="bubble_expanded_view_phone_landscape_overflow_width">412dp</dimen> <!-- Inset to apply to the icon in the overflow button. --> <dimen name="bubble_overflow_icon_inset">30dp</dimen> <!-- Default (and minimum) height of bubble overflow --> @@ -149,9 +158,17 @@ <!-- Extra padding around the dismiss target for bubbles --> <dimen name="bubble_dismiss_slop">16dp</dimen> <!-- Height of button allowing users to adjust settings for bubbles. --> - <dimen name="bubble_manage_button_height">56dp</dimen> + <dimen name="bubble_manage_button_height">36dp</dimen> + <!-- Height of manage button including margins. --> + <dimen name="bubble_manage_button_total_height">68dp</dimen> + <!-- The margin around the outside of the manage button. --> + <dimen name="bubble_manage_button_margin">16dp</dimen> <!-- Height of an item in the bubble manage menu. --> <dimen name="bubble_menu_item_height">60dp</dimen> + <!-- Padding applied to the bubble manage menu. --> + <dimen name="bubble_menu_padding">16dp</dimen> + <!-- Size of the icons in the manage menu. --> + <dimen name="bubble_menu_icon_size">24dp</dimen> <!-- Max width of the message bubble--> <dimen name="bubble_message_max_width">144dp</dimen> <!-- Min width of the message bubble --> @@ -174,14 +191,8 @@ <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> <dimen name="bubble_manage_menu_elevation">4dp</dimen> - - <!-- Bubbles user education views --> - <dimen name="bubbles_manage_education_width">160dp</dimen> - <!-- The inset from the top bound of the manage button to place the user education. --> - <dimen name="bubbles_manage_education_top_inset">65dp</dimen> - <!-- Size of padding for the user education cling, this should at minimum be larger than - individual_bubble_size + some padding. --> - <dimen name="bubble_stack_user_education_side_inset">72dp</dimen> + <!-- Size of user education views on large screens (phone is just match parent). --> + <dimen name="bubbles_user_education_width_large_screen">400dp</dimen> <!-- The width/height of the size compat restart button. --> <dimen name="size_compat_button_size">48dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index fffcd33f7992..cb6d4de71a45 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -32,8 +32,9 @@ <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">10dp</item> + <item name="android:layout_height">@dimen/split_divider_bar_width</item> <item name="android:layout_gravity">center_vertical</item> + <item name="android:background">@color/split_divider_background</item> </style> <style name="DockedDividerMinimizedShadow"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java new file mode 100644 index 000000000000..14ba9df93f24 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell; + +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; + +import androidx.annotation.NonNull; + +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.Executor; + +/** Display area organizer for the root display areas */ +public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { + + private static final String TAG = RootDisplayAreaOrganizer.class.getSimpleName(); + + /** {@link DisplayAreaInfo} list, which is mapped by display IDs. */ + private final SparseArray<DisplayAreaInfo> mDisplayAreasInfo = new SparseArray<>(); + /** Display area leashes, which is mapped by display IDs. */ + private final SparseArray<SurfaceControl> mLeashes = new SparseArray<>(); + + public RootDisplayAreaOrganizer(Executor executor) { + super(executor); + List<DisplayAreaAppearedInfo> infos = registerOrganizer(FEATURE_ROOT); + for (int i = infos.size() - 1; i >= 0; --i) { + onDisplayAreaAppeared(infos.get(i).getDisplayAreaInfo(), infos.get(i).getLeash()); + } + } + + public void attachToDisplayArea(int displayId, SurfaceControl.Builder b) { + final SurfaceControl sc = mLeashes.get(displayId); + if (sc != null) { + b.setParent(sc); + } + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + if (displayAreaInfo.featureId != FEATURE_ROOT) { + throw new IllegalArgumentException( + "Unknown feature: " + displayAreaInfo.featureId + + "displayAreaInfo:" + displayAreaInfo); + } + + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) != null) { + throw new IllegalArgumentException( + "Duplicate DA for displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.put(displayId, displayAreaInfo); + mLeashes.put(displayId, leash); + } + + @Override + public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) == null) { + throw new IllegalArgumentException( + "onDisplayAreaVanished() Unknown DA displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.remove(displayId); + } + + @Override + public void onDisplayAreaInfoChanged(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) == null) { + throw new IllegalArgumentException( + "onDisplayAreaInfoChanged() Unknown DA displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.put(displayId, displayAreaInfo); + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } + + @Override + public String toString() { + return TAG + "#" + mDisplayAreasInfo.size(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 34c66a4f4b82..bf074b0337ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -97,6 +97,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { b.setParent(sc); } + public void setPosition(@NonNull SurfaceControl.Transaction tx, int displayId, int x, int y) { + final SurfaceControl sc = mLeashes.get(displayId); + if (sc == null) { + throw new IllegalArgumentException("can't find display" + displayId); + } + tx.setPosition(sc, x, y); + } + @Override public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java index 0b941b59b3db..9113c79d40f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -103,6 +103,8 @@ public final class ShellCommandHandlerImpl { return runMoveToSideStage(args, pw); case "removeFromSideStage": return runRemoveFromSideStage(args, pw); + case "setSideStageOutline": + return runSetSideStageOutline(args, pw); case "setSideStagePosition": return runSetSideStagePosition(args, pw); case "setSideStageVisibility": @@ -161,6 +163,18 @@ public final class ShellCommandHandlerImpl { return true; } + private boolean runSetSideStageOutline(String[] args, PrintWriter pw) { + if (args.length < 3) { + // First arguments are "WMShell" and command name. + pw.println("Error: whether to enable or disable side stage outline border should be" + + " provided as arguments"); + return false; + } + final boolean enable = new Boolean(args[2]); + mSplitScreenOptional.ifPresent(split -> split.setSideStageOutline(enable)); + return true; + } + private boolean runSetSideStagePosition(String[] args, PrintWriter pw) { if (args.length < 3) { // First arguments are "WMShell" and command name. @@ -175,7 +189,7 @@ public final class ShellCommandHandlerImpl { private boolean runSetSideStageVisibility(String[] args, PrintWriter pw) { if (args.length < 3) { // First arguments are "WMShell" and command name. - pw.println("Error: side stage position should be provided as arguments"); + pw.println("Error: side stage visibility should be provided as arguments"); return false; } final Boolean visible = new Boolean(args[2]); @@ -197,6 +211,8 @@ public final class ShellCommandHandlerImpl { pw.println(" Move a task with given id in split-screen mode."); pw.println(" removeFromSideStage <taskId>"); pw.println(" Remove a task with given id in split-screen mode."); + pw.println(" setSideStageOutline <true/false>"); + pw.println(" Enable/Disable outline on the side-stage."); pw.println(" setSideStagePosition <SideStagePosition>"); pw.println(" Sets the position of the side-stage."); pw.println(" setSideStageVisibility <true/false>"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index d1fbf31e2b99..fa58fcda3d3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -20,10 +20,15 @@ import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCR import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.fullscreen.FullscreenUnfoldController; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -38,7 +43,9 @@ import java.util.Optional; public class ShellInitImpl { private static final String TAG = ShellInitImpl.class.getSimpleName(); + private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private final DragAndDropController mDragAndDropController; private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<BubbleController> mBubblesOptional; @@ -47,13 +54,18 @@ public class ShellInitImpl { private final Optional<AppPairsController> mAppPairsOptional; private final Optional<PipTouchHandler> mPipTouchHandlerOptional; private final FullscreenTaskListener mFullscreenTaskListener; + private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController; + private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional; private final ShellExecutor mMainExecutor; private final Transitions mTransitions; private final StartingWindowController mStartingWindow; private final InitImpl mImpl = new InitImpl(); - public ShellInitImpl(DisplayImeController displayImeController, + public ShellInitImpl( + DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, Optional<BubbleController> bubblesOptional, @@ -62,10 +74,14 @@ public class ShellInitImpl { Optional<AppPairsController> appPairsOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, + Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController, + Optional<Optional<FreeformTaskListener>> freeformTaskListenerOptional, Transitions transitions, StartingWindowController startingWindow, ShellExecutor mainExecutor) { + mDisplayController = displayController; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; mBubblesOptional = bubblesOptional; @@ -74,6 +90,8 @@ public class ShellInitImpl { mAppPairsOptional = appPairsOptional; mFullscreenTaskListener = fullscreenTaskListener; mPipTouchHandlerOptional = pipTouchHandlerOptional; + mFullscreenUnfoldController = fullscreenUnfoldTransitionController; + mFreeformTaskListenerOptional = freeformTaskListenerOptional.flatMap(f -> f); mTransitions = transitions; mMainExecutor = mainExecutor; mStartingWindow = startingWindow; @@ -84,7 +102,9 @@ public class ShellInitImpl { } private void init() { - // Start listening for display changes + // Start listening for display and insets changes + mDisplayController.initialize(); + mDisplayInsetsController.initialize(); mDisplayImeController.startMonitorDisplays(); // Setup the shell organizer @@ -108,6 +128,13 @@ public class ShellInitImpl { // controller instead of the feature interface, can just initialize the touch handler if // needed mPipTouchHandlerOptional.ifPresent((handler) -> handler.init()); + + // Initialize optional freeform + mFreeformTaskListenerOptional.ifPresent(f -> + mShellTaskOrganizer.addListenerForType( + f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM)); + + mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init); } @ExternalThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 656bdff0c782..020ecb7186ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -31,6 +31,7 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.content.LocusId; +import android.content.pm.ActivityInfo; import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; @@ -41,11 +42,13 @@ import android.util.SparseArray; import android.view.SurfaceControl; import android.window.ITaskOrganizerController; import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.sizecompatui.SizeCompatUIController; @@ -71,12 +74,14 @@ public class ShellTaskOrganizer extends TaskOrganizer implements public static final int TASK_LISTENER_TYPE_FULLSCREEN = -2; public static final int TASK_LISTENER_TYPE_MULTI_WINDOW = -3; public static final int TASK_LISTENER_TYPE_PIP = -4; + public static final int TASK_LISTENER_TYPE_FREEFORM = -5; @IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = { TASK_LISTENER_TYPE_UNDEFINED, TASK_LISTENER_TYPE_FULLSCREEN, TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_PIP, + TASK_LISTENER_TYPE_FREEFORM, }) public @interface TaskListenerType {} @@ -229,14 +234,14 @@ public class ShellTaskOrganizer extends TaskOrganizer implements + " already exists"); } mTaskListeners.put(listenerType, listener); + } - // Notify the listener of all existing tasks with the given type. - for (int i = mTasks.size() - 1; i >= 0; --i) { - final TaskAppearedInfo data = mTasks.valueAt(i); - final TaskListener taskListener = getTaskListener(data.getTaskInfo()); - if (taskListener != listener) continue; - listener.onTaskAppeared(data.getTaskInfo(), data.getLeash()); - } + // Notify the listener of all existing tasks with the given type. + for (int i = mTasks.size() - 1; i >= 0; --i) { + final TaskAppearedInfo data = mTasks.valueAt(i); + final TaskListener taskListener = getTaskListener(data.getTaskInfo()); + if (taskListener != listener) continue; + listener.onTaskAppeared(data.getTaskInfo(), data.getLeash()); } } } @@ -262,8 +267,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements tasks.add(data); } - // Remove listener - mTaskListeners.removeAt(index); + // Remove listener, there can be the multiple occurrences, so search the whole list. + for (int i = mTaskListeners.size() - 1; i >= 0; --i) { + if (mTaskListeners.valueAt(i) == listener) { + mTaskListeners.removeAt(i); + } + } // Associate tasks with new listeners if needed. for (int i = tasks.size() - 1; i >= 0; --i) { @@ -314,10 +323,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override - public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { if (mStartingWindow != null) { - mStartingWindow.removeStartingWindow(taskId, leash, frame, playRevealAnimation); + mStartingWindow.removeStartingWindow(removalInfo); } } @@ -493,14 +501,40 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override + public void onSizeCompatRestartButtonAppeared(int taskId) { + final TaskAppearedInfo info; + synchronized (mLock) { + info = mTasks.get(taskId); + } + if (info == null) { + return; + } + logSizeCompatRestartButtonEventReported(info, + FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED); + } + + @Override public void onSizeCompatRestartButtonClicked(int taskId) { final TaskAppearedInfo info; synchronized (mLock) { info = mTasks.get(taskId); } - if (info != null) { - restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); + if (info == null) { + return; + } + logSizeCompatRestartButtonEventReported(info, + FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED); + restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); + } + + private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, + int event) { + ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; + if (topActivityInfo == null) { + return; } + FrameworkStatsLog.write(FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED, + topActivityInfo.applicationInfo.uid, event); } /** @@ -579,6 +613,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements case WINDOWING_MODE_PINNED: return TASK_LISTENER_TYPE_PIP; case WINDOWING_MODE_FREEFORM: + return TASK_LISTENER_TYPE_FREEFORM; case WINDOWING_MODE_UNDEFINED: default: return TASK_LISTENER_TYPE_UNDEFINED; @@ -593,6 +628,8 @@ public class ShellTaskOrganizer extends TaskOrganizer implements return "TASK_LISTENER_TYPE_MULTI_WINDOW"; case TASK_LISTENER_TYPE_PIP: return "TASK_LISTENER_TYPE_PIP"; + case TASK_LISTENER_TYPE_FREEFORM: + return "TASK_LISTENER_TYPE_FREEFORM"; case TASK_LISTENER_TYPE_UNDEFINED: return "TASK_LISTENER_TYPE_UNDEFINED"; default: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index 1861e48482b8..2f3214d1d1ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -40,6 +40,8 @@ import android.view.ViewTreeObserver; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.wm.shell.common.SyncTransactionQueue; + import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -74,6 +76,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final ShellTaskOrganizer mTaskOrganizer; private final Executor mShellExecutor; + private final SyncTransactionQueue mSyncQueue; private ActivityManager.RunningTaskInfo mTaskInfo; private WindowContainerToken mTaskToken; @@ -89,11 +92,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final Rect mTmpRootRect = new Rect(); private final int[] mTmpLocation = new int[2]; - public TaskView(Context context, ShellTaskOrganizer organizer) { + public TaskView(Context context, ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) { super(context, null, 0, 0, true /* disableBackgroundLayer */); mTaskOrganizer = organizer; mShellExecutor = organizer.getExecutor(); + mSyncQueue = syncQueue; setUseAlpha(); getHolder().addCallback(this); mGuard.open("release"); @@ -189,8 +193,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mTaskToken, mTmpRect); - // TODO(b/151449487): Enable synchronization - mTaskOrganizer.applyTransaction(wct); + mSyncQueue.queue(wct); } /** @@ -236,14 +239,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private void updateTaskVisibility() { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); - mTaskOrganizer.applyTransaction(wct); - // TODO(b/151449487): Only call callback once we enable synchronization - if (mListener != null) { - final int taskId = mTaskInfo.taskId; + mSyncQueue.queue(wct); + if (mListener == null) { + return; + } + int taskId = mTaskInfo.taskId; + mSyncQueue.runInSync((t) -> { mListenerExecutor.execute(() -> { mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated); }); - } + }); } @Override @@ -264,10 +269,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, updateTaskVisibility(); } mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true); - // TODO: Synchronize show with the resize onLocationChanged(); if (taskInfo.taskDescription != null) { - setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + int backgroundColor = taskInfo.taskDescription.getBackgroundColor(); + mSyncQueue.runInSync((t) -> { + setResizeBackgroundColor(t, backgroundColor); + }); } if (mListener != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java index 58ca1fbaba24..8286d102791e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java @@ -20,8 +20,8 @@ import android.annotation.UiContext; import android.content.Context; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.annotations.ShellMainThread; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -30,12 +30,14 @@ import java.util.function.Consumer; public class TaskViewFactoryController { private final ShellTaskOrganizer mTaskOrganizer; private final ShellExecutor mShellExecutor; + private final SyncTransactionQueue mSyncQueue; private final TaskViewFactory mImpl = new TaskViewFactoryImpl(); public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer, - ShellExecutor shellExecutor) { + ShellExecutor shellExecutor, SyncTransactionQueue syncQueue) { mTaskOrganizer = taskOrganizer; mShellExecutor = shellExecutor; + mSyncQueue = syncQueue; } public TaskViewFactory asTaskViewFactory() { @@ -44,7 +46,7 @@ public class TaskViewFactoryController { /** Creates an {@link TaskView} */ public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) { - TaskView taskView = new TaskView(context, mTaskOrganizer); + TaskView taskView = new TaskView(context, mTaskOrganizer, mSyncQueue); executor.execute(() -> { onCreate.accept(taskView); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 8aca01d2467b..2aead9392e59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -62,4 +62,10 @@ public class Interpolators { */ public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f, 1); + + public static final PathInterpolator SLOWDOWN_INTERPOLATOR = + new PathInterpolator(0.5f, 1f, 0.5f, 1f); + + public static final PathInterpolator DIM_INTERPOLATOR = + new PathInterpolator(.23f, .87f, .52f, -0.11f); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index e6d088e6537d..6a252e0d7dcb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -19,6 +19,7 @@ package com.android.wm.shell.apppairs; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; @@ -26,7 +27,6 @@ import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEF import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import android.app.ActivityManager; -import android.graphics.Rect; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -39,9 +39,11 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SurfaceUtils; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.common.split.SplitWindowManager; import java.io.PrintWriter; @@ -67,13 +69,33 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou private final SyncTransactionQueue mSyncQueue; private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private SplitLayout mSplitLayout; + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = + new SplitWindowManager.ParentContainerCallbacks() { + @Override + public void attachToParentSurface(SurfaceControl.Builder b) { + b.setParent(mRootTaskLeash); + } + + @Override + public void onLeashReady(SurfaceControl leash) { + mSyncQueue.runInSync(t -> t + .show(leash) + .setLayer(leash, SPLIT_DIVIDER_LAYER) + .setPosition(leash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top)); + } + }; + AppPair(AppPairsController controller) { mController = controller; mSyncQueue = controller.getSyncTransactionQueue(); mDisplayController = controller.getDisplayController(); mDisplayImeController = controller.getDisplayImeController(); + mDisplayInsetsController = controller.getDisplayInsetsController(); } int getRootTaskId() { @@ -109,8 +131,8 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou mSplitLayout = new SplitLayout(TAG + "SplitDivider", mDisplayController.getDisplayContext(mRootTaskInfo.displayId), mRootTaskInfo.configuration, this /* layoutChangeListener */, - b -> b.setParent(mRootTaskLeash), mDisplayImeController, - mController.getTaskOrganizer()); + mParentContainerCallbacks, mDisplayImeController, mController.getTaskOrganizer()); + mDisplayInsetsController.addInsetsChangedListener(mRootTaskInfo.displayId, mSplitLayout); final WindowContainerToken token1 = task1.token; final WindowContainerToken token2 = task2.token; @@ -176,21 +198,17 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou if (mTaskLeash1 == null || mTaskLeash2 == null) return; mSplitLayout.init(); - final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - final Rect dividerBounds = mSplitLayout.getDividerBounds(); - - // TODO: Is there more we need to do here? - mSyncQueue.runInSync(t -> { - t.setLayer(dividerLeash, Integer.MAX_VALUE) - .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, - mTaskInfo1.positionInParent.y) - .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, - mTaskInfo2.positionInParent.y) - .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) - .show(mRootTaskLeash) - .show(mTaskLeash1) - .show(mTaskLeash2); - }); + + mSyncQueue.runInSync(t -> t + .show(mRootTaskLeash) + .show(mTaskLeash1) + .show(mTaskLeash2) + .setPosition(mTaskLeash1, + mTaskInfo1.positionInParent.x, + mTaskInfo1.positionInParent.y) + .setPosition(mTaskLeash2, + mTaskInfo2.positionInParent.x, + mTaskInfo2.positionInParent.y)); } @Override @@ -214,7 +232,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou if (mSplitLayout != null && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { - onBoundsChanged(mSplitLayout); + onLayoutSizeChanged(mSplitLayout); } } else if (taskInfo.taskId == getTaskId1()) { mTaskInfo1 = taskInfo; @@ -295,17 +313,30 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou } @Override - public void onBoundsChanging(SplitLayout layout) { + public void onLayoutPositionChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> + layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); + } + + @Override + public void onLayoutSizeChanging(SplitLayout layout) { mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); } @Override - public void onBoundsChanged(SplitLayout layout) { + public void onLayoutSizeChanged(SplitLayout layout) { final WindowContainerTransaction wct = new WindowContainerTransaction(); layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); } + + @Override + public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2); + mController.getTaskOrganizer().applyTransaction(wct); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java index b159333e9a0e..53234ab971d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java @@ -29,6 +29,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -50,14 +51,17 @@ public class AppPairsController { private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>(); private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, DisplayController displayController, ShellExecutor mainExecutor, - DisplayImeController displayImeController) { + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController) { mTaskOrganizer = organizer; mSyncQueue = syncQueue; mDisplayController = displayController; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; } @@ -148,6 +152,10 @@ public class AppPairsController { return mDisplayImeController; } + DisplayInsetsController getDisplayInsetsController() { + return mDisplayInsetsController; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 9d65d28b21b4..8d43f1375a8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -121,7 +121,7 @@ public class Bubble implements BubbleViewProvider { @Nullable private Icon mIcon; private boolean mIsBubble; - private boolean mIsVisuallyInterruptive; + private boolean mIsTextChanged; private boolean mIsClearable; private boolean mShouldSuppressNotificationDot; private boolean mShouldSuppressNotificationList; @@ -342,12 +342,12 @@ public class Bubble implements BubbleViewProvider { } /** - * Sets whether this bubble is considered visually interruptive. This method is purely for + * Sets whether this bubble is considered text changed. This method is purely for * testing. */ @VisibleForTesting - void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { - mIsVisuallyInterruptive = visuallyInterruptive; + void setTextChangedForTest(boolean textChanged) { + mIsTextChanged = textChanged; } /** @@ -422,14 +422,6 @@ public class Bubble implements BubbleViewProvider { } } - @Override - public void setExpandedContentAlpha(float alpha) { - if (mExpandedView != null) { - mExpandedView.setAlpha(alpha); - mExpandedView.setTaskViewAlpha(alpha); - } - } - /** * Set visibility of bubble in the expanded state. * @@ -462,7 +454,7 @@ public class Bubble implements BubbleViewProvider { mFlyoutMessage = extractFlyoutMessage(entry); if (entry.getRanking() != null) { mShortcutInfo = entry.getRanking().getConversationShortcutInfo(); - mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); + mIsTextChanged = entry.getRanking().isTextChanged(); if (entry.getRanking().getChannel() != null) { mIsImportantConversation = entry.getRanking().getChannel().isImportantConversation(); @@ -503,8 +495,8 @@ public class Bubble implements BubbleViewProvider { return mIcon; } - boolean isVisuallyInterruptive() { - return mIsVisuallyInterruptive; + boolean isTextChanged() { + return mIsTextChanged; } /** 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 09fcb86e56de..b6d65bebff28 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 @@ -56,7 +56,6 @@ import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -70,6 +69,7 @@ import android.util.SparseArray; import android.util.SparseSetArray; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; import android.window.WindowContainerTransaction; @@ -85,6 +85,7 @@ import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.pip.PinnedStackListenerForwarder; @@ -97,7 +98,6 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -137,6 +137,7 @@ public class BubbleController { private final TaskStackListenerImpl mTaskStackListener; private final ShellTaskOrganizer mTaskOrganizer; private final DisplayController mDisplayController; + private final SyncTransactionQueue mSyncQueue; // Used to post to main UI thread private final ShellExecutor mMainExecutor; @@ -144,7 +145,6 @@ public class BubbleController { private BubbleLogger mLogger; private BubbleData mBubbleData; - private View mBubbleScrim; @Nullable private BubbleStackView mStackView; private BubbleIconFactory mBubbleIconFactory; private BubblePositioner mBubblePositioner; @@ -189,6 +189,9 @@ public class BubbleController { /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */ private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; + /** Saved insets, used to detect WindowInset changes. */ + private WindowInsets mWindowInsets; + private boolean mInflateSynchronously; /** True when user is in status bar unlock shade. */ @@ -209,7 +212,8 @@ public class BubbleController { ShellTaskOrganizer organizer, DisplayController displayController, ShellExecutor mainExecutor, - Handler mainHandler) { + Handler mainHandler, + SyncTransactionQueue syncQueue) { BubbleLogger logger = new BubbleLogger(uiEventLogger); BubblePositioner positioner = new BubblePositioner(context, windowManager); BubbleData data = new BubbleData(context, logger, positioner, mainExecutor); @@ -217,7 +221,7 @@ public class BubbleController { new BubbleDataRepository(context, launcherApps, mainExecutor), statusBarService, windowManager, windowManagerShellWrapper, launcherApps, logger, taskStackListener, organizer, positioner, displayController, mainExecutor, - mainHandler); + mainHandler, syncQueue); } /** @@ -239,7 +243,8 @@ public class BubbleController { BubblePositioner positioner, DisplayController displayController, ShellExecutor mainExecutor, - Handler mainHandler) { + Handler mainHandler, + SyncTransactionQueue syncQueue) { mContext = context; mLauncherApps = launcherApps; mBarService = statusBarService == null @@ -262,6 +267,7 @@ public class BubbleController { mSavedBubbleKeysPerUser = new SparseSetArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); mDisplayController = displayController; + mSyncQueue = syncQueue; } public void initialize() { @@ -561,6 +567,10 @@ public class BubbleController { return mTaskOrganizer; } + SyncTransactionQueue getSyncTransactionQueue() { + return mSyncQueue; + } + /** Contains information to help position things on the screen. */ BubblePositioner getPositioner() { return mBubblePositioner; @@ -572,7 +582,7 @@ public class BubbleController { /** * BubbleStackView is lazily created by this method the first time a Bubble is added. This - * method initializes the stack view and adds it to the StatusBar just above the scrim. + * method initializes the stack view and adds it to window manager. */ private void ensureStackViewCreated() { if (mStackView == null) { @@ -620,20 +630,31 @@ public class BubbleController { try { mAddedToWindowManager = true; mBubbleData.getOverflow().initialize(this); - mStackView.addView(mBubbleScrim); mWindowManager.addView(mStackView, mWmLayoutParams); - // Position info is dependent on us being attached to a window - mBubblePositioner.update(); + mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + mBubblePositioner.update(); + mStackView.onDisplaySizeChanged(); + } + return windowInsets; + }); } catch (IllegalStateException e) { // This means the stack has already been added. This shouldn't happen... e.printStackTrace(); } } - /** For the overflow to be focusable & receive key events the flags must be update. **/ - void updateWindowFlagsForOverflow(boolean showingOverflow) { + /** + * In some situations bubble's should be able to receive key events for back: + * - when the bubble overflow is showing + * - when the user education for the stack is showing. + * + * @param interceptBack whether back should be intercepted or not. + */ + void updateWindowFlagsForBackpress(boolean interceptBack) { if (mStackView != null && mAddedToWindowManager) { - mWmLayoutParams.flags = showingOverflow + mWmLayoutParams.flags = interceptBack ? 0 : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; @@ -652,7 +673,6 @@ public class BubbleController { mAddedToWindowManager = false; if (mStackView != null) { mWindowManager.removeView(mStackView); - mStackView.removeView(mBubbleScrim); mBubbleData.getOverflow().cleanUpExpandedState(); } else { Log.w(TAG, "StackView added to WindowManager, but was null when removing!"); @@ -754,13 +774,6 @@ public class BubbleController { } } - private void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) { - mBubbleScrim = view; - callback.accept(mMainExecutor, mMainExecutor.executeBlockingForResult(() -> { - return Looper.myLooper(); - }, Looper.class)); - } - private void setSysuiProxy(Bubbles.SysuiProxy proxy) { mSysuiProxy = proxy; } @@ -897,8 +910,7 @@ public class BubbleController { * Fills the overflow bubbles by loading them from disk. */ void loadOverflowBubblesFromDisk() { - if (!mBubbleData.getOverflowBubbles().isEmpty() && !mOverflowDataLoadNeeded) { - // we don't need to load overflow bubbles from disk if it is already in memory + if (!mOverflowDataLoadNeeded) { return; } mOverflowDataLoadNeeded = false; @@ -927,7 +939,7 @@ public class BubbleController { public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { // If this is an interruptive notif, mark that it's interrupted mSysuiProxy.setNotificationInterruption(notif.getKey()); - if (!notif.getRanking().visuallyInterruptive() + if (!notif.getRanking().isTextChanged() && (notif.getBubbleMetadata() != null && !notif.getBubbleMetadata().getAutoExpandBubble()) && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { @@ -1366,8 +1378,9 @@ public class BubbleController { private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mBubblePositioner.setImeVisible(imeVisible, imeHeight); if (mStackView != null) { - mStackView.onImeVisibilityChanged(imeVisible, imeHeight); + mStackView.animateForIme(imeVisible); } } } @@ -1566,13 +1579,6 @@ public class BubbleController { } @Override - public void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) { - mMainExecutor.execute(() -> { - BubbleController.this.setBubbleScrim(view, callback); - }); - } - - @Override public void setExpandListener(BubbleExpandListener listener) { mMainExecutor.execute(() -> { BubbleController.this.setExpandListener(listener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index d73ce6951e6d..519a856538c7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -323,7 +323,7 @@ public class BubbleData { } mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); - suppressFlyout |= !bubble.isVisuallyInterruptive(); + suppressFlyout |= !bubble.isTextChanged(); if (prevBubble == null) { // Create a new bubble @@ -558,6 +558,8 @@ public class BubbleData { } Bubble bubbleToRemove = mBubbles.get(indexToRemove); bubbleToRemove.stopInflation(); + overflowBubble(reason, bubbleToRemove); + if (mBubbles.size() == 1) { if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) { // No more active bubbles but we have stuff in the overflow -- select that view @@ -581,8 +583,6 @@ public class BubbleData { mStateChange.orderChanged |= repackAll(); } - overflowBubble(reason, bubbleToRemove); - // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. if (Objects.equals(mSelectedBubble, bubbleToRemove)) { // Move selection to the new bubble at the same position. @@ -699,10 +699,9 @@ public class BubbleData { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setSelectedBubbleInternal: " + bubble); } - if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { + if (Objects.equals(bubble, mSelectedBubble)) { return; } - // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey()); if (bubble != null && !mBubbles.contains(bubble) @@ -771,6 +770,10 @@ public class BubbleData { Log.e(TAG, "Attempt to expand stack without selected bubble!"); return; } + if (mSelectedBubble.getKey().equals(mOverflow.getKey()) && !mBubbles.isEmpty()) { + // Show previously selected bubble instead of overflow menu when expanding. + setSelectedBubbleInternal(mBubbles.get(0)); + } if (mSelectedBubble instanceof Bubble) { ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); } @@ -779,16 +782,6 @@ public class BubbleData { // Apply ordering and grouping rules from expanded -> collapsed, then save // the result. mStateChange.orderChanged |= repackAll(); - // Save the state which should be returned to when expanded (with no other changes) - - if (mShowingOverflow) { - // Show previously selected bubble instead of overflow menu on next expansion. - if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) { - setSelectedBubbleInternal(mSelectedBubble); - } else { - setSelectedBubbleInternal(mBubbles.get(0)); - } - } if (mBubbles.indexOf(mSelectedBubble) > 0) { // Move the selected bubble to the top while collapsed. int index = mBubbles.indexOf(mSelectedBubble); 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 9687ec6a8168..a87aad4261a6 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 @@ -25,6 +25,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -60,7 +61,6 @@ import android.widget.LinearLayout; import androidx.annotation.Nullable; import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import com.android.wm.shell.TaskView; import com.android.wm.shell.common.AlphaOptimizedButton; @@ -77,7 +77,6 @@ public class BubbleExpandedView extends LinearLayout { // The triangle pointing to the expanded view private View mPointerView; - private int mPointerMargin; @Nullable private int[] mExpandedViewContainerLocation; private AlphaOptimizedButton mManageButton; @@ -102,9 +101,6 @@ public class BubbleExpandedView extends LinearLayout { */ private boolean mIsAlphaAnimating = false; - private int mMinHeight; - private int mOverflowHeight; - private int mManageButtonHeight; private int mPointerWidth; private int mPointerHeight; private float mPointerRadius; @@ -232,7 +228,7 @@ public class BubbleExpandedView extends LinearLayout { @Override public void onBackPressedOnTaskRoot(int taskId) { if (mTaskId == taskId && mStackView.isExpanded()) { - mController.collapseStack(); + mStackView.onBackPressed(); } } }; @@ -338,7 +334,8 @@ public class BubbleExpandedView extends LinearLayout { bringChildToFront(mOverflowView); mManageButton.setVisibility(GONE); } else { - mTaskView = new TaskView(mContext, mController.getTaskOrganizer()); + mTaskView = new TaskView(mContext, mController.getTaskOrganizer(), + mController.getSyncTransactionQueue()); mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); mExpandedViewContainer.addView(mTaskView); bringChildToFront(mTaskView); @@ -347,12 +344,8 @@ public class BubbleExpandedView extends LinearLayout { void updateDimensions() { Resources res = getResources(); - mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); - mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); - updateFontSize(); - mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius); @@ -368,7 +361,6 @@ public class BubbleExpandedView extends LinearLayout { updatePointerView(); } - mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height); if (mManageButton != null) { int visibility = mManageButton.getVisibility(); removeView(mManageButton); @@ -406,6 +398,7 @@ public class BubbleExpandedView extends LinearLayout { updatePointerView(); } + /** Updates the size and visuals of the pointer. **/ private void updatePointerView() { LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { @@ -532,9 +525,8 @@ public class BubbleExpandedView extends LinearLayout { if (mTaskView != null) { mTaskView.setAlpha(alpha); } - if (mManageButton != null && mManageButton.getVisibility() == View.VISIBLE) { - mManageButton.setAlpha(alpha); - } + mPointerView.setAlpha(alpha); + setAlpha(alpha); } /** @@ -553,6 +545,7 @@ public class BubbleExpandedView extends LinearLayout { mIsContentVisible = visibility; if (mTaskView != null && !mIsAlphaAnimating) { mTaskView.setAlpha(visibility ? 1f : 0f); + mPointerView.setAlpha(visibility ? 1f : 0f); } } @@ -632,12 +625,11 @@ public class BubbleExpandedView extends LinearLayout { } if ((mBubble != null && mTaskView != null) || mIsOverflow) { - float desiredHeight = mIsOverflow - ? mPositioner.isLargeScreen() ? getMaxExpandedHeight() : mOverflowHeight - : mBubble.getDesiredHeight(mContext); - desiredHeight = Math.max(desiredHeight, mMinHeight); - float height = Math.min(desiredHeight, getMaxExpandedHeight()); - height = Math.max(height, mMinHeight); + float desiredHeight = mPositioner.getExpandedViewHeight(mBubble); + int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow); + float height = desiredHeight == MAX_HEIGHT + ? maxHeight + : Math.min(desiredHeight, maxHeight); FrameLayout.LayoutParams lp = mIsOverflow ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams() : (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); @@ -661,23 +653,6 @@ public class BubbleExpandedView extends LinearLayout { } } - private int getMaxExpandedHeight() { - int expandedContainerY = mExpandedViewContainerLocation != null - // Remove top insets back here because availableRect.height would account for that - ? mExpandedViewContainerLocation[1] - mPositioner.getInsets().top - : 0; - int settingsHeight = mIsOverflow ? 0 : mManageButtonHeight; - int pointerHeight = mPositioner.showBubblesVertically() - ? mPointerWidth - : (int) (mPointerHeight - mPointerOverlap + mPointerMargin); - return mPositioner.getAvailableRect().height() - - expandedContainerY - - getPaddingTop() - - getPaddingBottom() - - settingsHeight - - pointerHeight; - } - /** * Update appearance of the expanded view being displayed. * @@ -715,28 +690,29 @@ public class BubbleExpandedView extends LinearLayout { * the bubble if showing vertically. * @param onLeft whether the stack was on the left side of the screen when expanded. */ - public void setPointerPosition(float bubblePosition, boolean onLeft) { + public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) { // Pointer gets drawn in the padding final boolean showVertically = mPositioner.showBubblesVertically(); final float paddingLeft = (showVertically && onLeft) ? mPointerHeight - mPointerOverlap : 0; final float paddingRight = (showVertically && !onLeft) - ? mPointerHeight - mPointerOverlap : 0; - final float paddingTop = showVertically ? 0 + ? mPointerHeight - mPointerOverlap + : 0; + final float paddingTop = showVertically + ? 0 : mPointerHeight - mPointerOverlap; setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0); - final float expandedViewY = mPositioner.getExpandedViewY(); - // TODO: I don't understand why it works but it does - why normalized in portrait - // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? - final float normalizedSize = IconNormalizer.getNormalizedCircleSize( - mPositioner.getBubbleSize()); - final float bubbleCenter = showVertically - ? bubblePosition + (mPositioner.getBubbleSize() / 2f) - expandedViewY - : bubblePosition + (normalizedSize / 2f) - mPointerWidth; + // Subtract the expandedViewY here because the pointer is placed within the expandedView. + float pointerPosition = mPositioner.getPointerPosition(bubblePosition); + final float bubbleCenter = mPositioner.showBubblesVertically() + ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition) + : pointerPosition; // Post because we need the width of the view post(() -> { + mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; + updatePointerView(); float pointerY; float pointerX; if (showVertically) { @@ -748,11 +724,13 @@ public class BubbleExpandedView extends LinearLayout { pointerY = mPointerOverlap; pointerX = bubbleCenter - (mPointerWidth / 2f); } - mPointerView.setTranslationY(pointerY); - mPointerView.setTranslationX(pointerX); - mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; - updatePointerView(); - mPointerView.setVisibility(VISIBLE); + if (animate) { + mPointerView.animate().translationX(pointerX).translationY(pointerY).start(); + } else { + mPointerView.setTranslationY(pointerY); + mPointerView.setTranslationX(pointerX); + mPointerView.setVisibility(VISIBLE); + } }); } @@ -764,6 +742,10 @@ public class BubbleExpandedView extends LinearLayout { mManageButton.getBoundsOnScreen(rect); } + public int getManageButtonMargin() { + return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart(); + } + /** * Cleans up anything related to the task and {@code TaskView}. If this view should be reused * after this method is called, then diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java index 35a4f33ecf72..9374da4c4fab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java @@ -56,9 +56,6 @@ import com.android.wm.shell.common.TriangleShape; * transform into the 'new' dot, which is used during flyout dismiss animations/gestures. */ public class BubbleFlyoutView extends FrameLayout { - /** Max width of the flyout, in terms of percent of the screen width. */ - private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; - /** Translation Y of fade animation. */ private static final float FLYOUT_FADE_Y = 40f; @@ -68,6 +65,8 @@ public class BubbleFlyoutView extends FrameLayout { // Whether the flyout view should show a pointer to the bubble. private static final boolean SHOW_POINTER = false; + private BubblePositioner mPositioner; + private final int mFlyoutPadding; private final int mFlyoutSpaceFromBubble; private final int mPointerSize; @@ -156,10 +155,11 @@ public class BubbleFlyoutView extends FrameLayout { /** Callback to run when the flyout is hidden. */ @Nullable private Runnable mOnHide; - public BubbleFlyoutView(Context context) { + public BubbleFlyoutView(Context context, BubblePositioner positioner) { super(context); - LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); + mPositioner = positioner; + LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container); mSenderText = findViewById(R.id.bubble_flyout_name); mSenderAvatar = findViewById(R.id.bubble_flyout_avatar); @@ -230,11 +230,11 @@ public class BubbleFlyoutView extends FrameLayout { /* * Fade animation for consecutive flyouts. */ - void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, PointF stackPos, + void animateUpdate(Bubble.FlyoutMessage flyoutMessage, PointF stackPos, boolean hideDot, Runnable onHide) { mOnHide = onHide; final Runnable afterFadeOut = () -> { - updateFlyoutMessage(flyoutMessage, parentWidth); + updateFlyoutMessage(flyoutMessage); // Wait for TextViews to layout with updated height. post(() -> { fade(true /* in */, stackPos, hideDot, () -> {} /* after */); @@ -266,7 +266,7 @@ public class BubbleFlyoutView extends FrameLayout { .withEndAction(afterFade); } - private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) { + private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage) { final Drawable senderAvatar = flyoutMessage.senderAvatar; if (senderAvatar != null && flyoutMessage.isGroupChat) { mSenderAvatar.setVisibility(VISIBLE); @@ -278,8 +278,7 @@ public class BubbleFlyoutView extends FrameLayout { mSenderText.setTranslationX(0); } - final int maxTextViewWidth = - (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2; + final int maxTextViewWidth = (int) mPositioner.getMaxFlyoutSize() - mFlyoutPadding * 2; // Name visibility if (!TextUtils.isEmpty(flyoutMessage.senderName)) { @@ -328,22 +327,20 @@ public class BubbleFlyoutView extends FrameLayout { void setupFlyoutStartingAsDot( Bubble.FlyoutMessage flyoutMessage, PointF stackPos, - float parentWidth, boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete, @Nullable Runnable onHide, float[] dotCenter, - boolean hideDot, - BubblePositioner positioner) { + boolean hideDot) { - mBubbleSize = positioner.getBubbleSize(); + mBubbleSize = mPositioner.getBubbleSize(); mOriginalDotSize = SIZE_PERCENTAGE * mBubbleSize; mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f; mNewDotSize = mNewDotRadius * 2f; - updateFlyoutMessage(flyoutMessage, parentWidth); + updateFlyoutMessage(flyoutMessage); mArrowPointingLeft = arrowPointingLeft; mDotColor = dotColor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 705a12a5e65b..0c3a6b2dbd84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -154,10 +154,6 @@ class BubbleOverflow( return dotPath } - override fun setExpandedContentAlpha(alpha: Float) { - expandedView?.alpha = alpha - } - override fun setTaskViewVisibility(visible: Boolean) { // Overflow does not have a TaskView. } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java index ede42285d9cd..5e9d97f23c57 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java @@ -142,7 +142,7 @@ public class BubbleOverflowContainerView extends LinearLayout { super.onAttachedToWindow(); if (mController != null) { // For the overflow to get key events (e.g. back press) we need to adjust the flags - mController.updateWindowFlagsForOverflow(true); + mController.updateWindowFlagsForBackpress(true); } setOnKeyListener(mKeyListener); } @@ -151,7 +151,7 @@ public class BubbleOverflowContainerView extends LinearLayout { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mController != null) { - mController.updateWindowFlagsForOverflow(false); + mController.updateWindowFlagsForBackpress(false); } setOnKeyListener(null); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index c600f56ba0c5..127d5a8a9966 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -34,6 +34,7 @@ import android.view.WindowMetrics; import androidx.annotation.VisibleForTesting; +import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import java.lang.annotation.Retention; @@ -58,29 +59,48 @@ public class BubblePositioner { /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ public static final int NUM_VISIBLE_WHEN_RESTING = 2; + /** Indicates a bubble's height should be the maximum available space. **/ + public static final int MAX_HEIGHT = -1; + /** The max percent of screen width to use for the flyout on large screens. */ + public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f; + /** The max percent of screen width to use for the flyout on phone. */ + public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f; + /** The percent of screen width that should be used for the expanded view on a large screen. **/ + public static final float EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT = 0.72f; private Context mContext; private WindowManager mWindowManager; - private Rect mPositionRect; + private Rect mScreenRect; private @Surface.Rotation int mRotation = Surface.ROTATION_0; private Insets mInsets; + private boolean mImeVisible; + private int mImeHeight; + private boolean mIsLargeScreen; + + private Rect mPositionRect; private int mDefaultMaxBubbles; private int mMaxBubbles; - private int mBubbleSize; - private int mBubbleBadgeSize; private int mSpacingBetweenBubbles; + + private int mExpandedViewMinHeight; private int mExpandedViewLargeScreenWidth; + private int mExpandedViewLargeScreenInset; + + private int mOverflowWidth; private int mExpandedViewPadding; private int mPointerMargin; - private float mPointerWidth; - private float mPointerHeight; + private int mPointerWidth; + private int mPointerHeight; + private int mPointerOverlap; + private int mManageButtonHeight; + private int mOverflowHeight; + private int mMinimumFlyoutWidthLargeScreen; private PointF mPinLocation; private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; - private boolean mIsLargeScreen; private boolean mShowingInTaskbar; private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; private int mTaskbarIconSize; @@ -143,6 +163,7 @@ public class BubblePositioner { mRotation = rotation; mInsets = insets; + mScreenRect = new Rect(bounds); mPositionRect = new Rect(bounds); mPositionRect.left += mInsets.left; mPositionRect.top += mInsets.top; @@ -151,16 +172,27 @@ public class BubblePositioner { Resources res = mContext.getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); - mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size); mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); - - mExpandedViewLargeScreenWidth = res.getDimensionPixelSize( - R.dimen.bubble_expanded_view_tablet_width); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + mExpandedViewLargeScreenWidth = (int) (bounds.width() + * EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT); + mExpandedViewLargeScreenInset = mIsLargeScreen + ? (bounds.width() - mExpandedViewLargeScreenWidth) / 2 + : mExpandedViewPadding; + mOverflowWidth = mIsLargeScreen + ? mExpandedViewLargeScreenWidth + : res.getDimensionPixelSize( + R.dimen.bubble_expanded_view_phone_landscape_overflow_width); mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); + mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap); + mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height); + mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); + mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); + mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize( + R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); @@ -225,6 +257,13 @@ public class BubblePositioner { } /** + * @return a rect of the screen size. + */ + public Rect getScreenRect() { + return mScreenRect; + } + + /** * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its * inset is not included here. */ @@ -265,48 +304,287 @@ public class BubblePositioner { return mMaxBubbles; } + /** The height for the IME if it's visible. **/ + public int getImeHeight() { + return mImeVisible ? mImeHeight : 0; + } + + /** Sets whether the IME is visible. **/ + public void setImeVisible(boolean visible, int height) { + mImeVisible = visible; + mImeHeight = height; + } + /** - * Calculates the left & right padding for the bubble expanded view. + * Calculates the padding for the bubble expanded view. * - * On larger screens the width of the expanded view is restricted via this padding. - * On landscape the bubble overflow expanded view is also restricted via this padding. + * Some specifics: + * On large screens the width of the expanded view is restricted via this padding. + * On phone landscape the bubble overflow expanded view is also restricted via this padding. + * On large screens & landscape no top padding is set, the top position is set via translation. + * On phone portrait top padding is set as the space between the tip of the pointer and the + * bubble. + * When the overflow is shown it doesn't have the manage button to pad out the bottom so + * padding is added. */ - public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) { - int leftPadding = mInsets.left + mExpandedViewPadding; - int rightPadding = mInsets.right + mExpandedViewPadding; - final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow; - if (showBubblesVertically()) { - if (!onLeft) { - rightPadding += mBubbleSize - mPointerHeight; - leftPadding += isLargeOrOverflow - ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth) - : 0; - } else { - leftPadding += mBubbleSize - mPointerHeight; - rightPadding += isLargeOrOverflow - ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth) - : 0; + public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) { + final int pointerTotalHeight = mPointerHeight - mPointerOverlap; + if (mIsLargeScreen) { + // [left, top, right, bottom] + mPaddings[0] = onLeft + ? mExpandedViewLargeScreenInset - pointerTotalHeight + : mExpandedViewLargeScreenInset; + mPaddings[1] = 0; + mPaddings[2] = onLeft + ? mExpandedViewLargeScreenInset + : mExpandedViewLargeScreenInset - pointerTotalHeight; + // Overflow doesn't show manage button / get padding from it so add padding here for it + mPaddings[3] = isOverflow ? mExpandedViewPadding : 0; + return mPaddings; + } else { + int leftPadding = mInsets.left + mExpandedViewPadding; + int rightPadding = mInsets.right + mExpandedViewPadding; + final float expandedViewWidth = isOverflow + ? mOverflowWidth + : mExpandedViewLargeScreenWidth; + if (showBubblesVertically()) { + if (!onLeft) { + rightPadding += mBubbleSize - pointerTotalHeight; + leftPadding += isOverflow + ? (mPositionRect.width() - rightPadding - expandedViewWidth) + : 0; + } else { + leftPadding += mBubbleSize - pointerTotalHeight; + rightPadding += isOverflow + ? (mPositionRect.width() - leftPadding - expandedViewWidth) + : 0; + } } + // [left, top, right, bottom] + mPaddings[0] = leftPadding; + mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin; + mPaddings[2] = rightPadding; + mPaddings[3] = 0; + return mPaddings; } - // [left, top, right, bottom] - mPaddings[0] = leftPadding; - mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin; - mPaddings[2] = rightPadding; - mPaddings[3] = 0; - return mPaddings; } - /** Calculates the y position of the expanded view when it is expanded. */ - public float getExpandedViewY() { + /** Gets the y position of the expanded view if it was top-aligned. */ + public float getExpandedViewYTopAligned() { final int top = getAvailableRect().top; if (showBubblesVertically()) { - return top - mPointerWidth; + return top - mPointerWidth + mExpandedViewPadding; } else { return top + mBubbleSize + mPointerMargin; } } /** + * Calculate the maximum height the expanded view can be depending on where it's placed on + * the screen and the size of the elements around it (e.g. padding, pointer, manage button). + */ + public int getMaxExpandedViewHeight(boolean isOverflow) { + // Subtract top insets because availableRect.height would account for that + int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top; + int paddingTop = showBubblesVertically() + ? 0 + : mPointerHeight; + // Subtract pointer size because it's laid out in LinearLayout with the expanded view. + int pointerSize = showBubblesVertically() + ? mPointerWidth + : (mPointerHeight + mPointerMargin); + int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight; + return getAvailableRect().height() + - expandedContainerY + - paddingTop + - pointerSize + - bottomPadding; + } + + /** + * Determines the height for the bubble, ensuring a minimum height. If the height should be as + * big as available, returns {@link #MAX_HEIGHT}. + */ + public float getExpandedViewHeight(BubbleViewProvider bubble) { + boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); + if (isOverflow && showBubblesVertically() && !mIsLargeScreen) { + // overflow in landscape on phone is max + return MAX_HEIGHT; + } + float desiredHeight = isOverflow + ? mOverflowHeight + : ((Bubble) bubble).getDesiredHeight(mContext); + desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight); + if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) { + return MAX_HEIGHT; + } + return desiredHeight; + } + + /** + * Gets the y position for the expanded view. This is the position on screen of the top + * horizontal line of the expanded view. + * + * @param bubble the bubble being positioned. + * @param bubblePosition the x position of the bubble if showing on top, the y position of the + * bubble if showing vertically. + * @return the y position for the expanded view. + */ + public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) { + boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); + float expandedViewHeight = getExpandedViewHeight(bubble); + float topAlignment = getExpandedViewYTopAligned(); + if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) { + // Top-align when bubbles are shown at the top or are max size. + return topAlignment; + } + // If we're here, we're showing vertically & developer has made height less than maximum. + int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight; + float pointerPosition = getPointerPosition(bubblePosition); + float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight; + float topIfCentered = pointerPosition - (expandedViewHeight / 2); + if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) { + // Center it + return pointerPosition - mPointerWidth - (expandedViewHeight / 2f); + } else if (topIfCentered <= mPositionRect.top) { + // Top align + return topAlignment; + } else { + // Bottom align + return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth; + } + } + + /** + * The position the pointer points to, the center of the bubble. + * + * @param bubblePosition the x position of the bubble if showing on top, the y position of the + * bubble if showing vertically. + * @return the position the tip of the pointer points to. The x position if showing on top, the + * y position if showing vertically. + */ + public float getPointerPosition(float bubblePosition) { + // TODO: I don't understand why it works but it does - why normalized in portrait + // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? + final float normalizedSize = IconNormalizer.getNormalizedCircleSize( + getBubbleSize()); + return showBubblesVertically() + ? bubblePosition + (getBubbleSize() / 2f) + : bubblePosition + (normalizedSize / 2f) - mPointerWidth; + } + + private int getExpandedStackSize(int numberOfBubbles) { + return (numberOfBubbles * mBubbleSize) + + ((numberOfBubbles - 1) * mSpacingBetweenBubbles); + } + + /** + * Returns the position of the bubble on-screen when the stack is expanded. + * + * @param index the index of the bubble in the stack. + * @param state state information about the stack to help with calculations. + * @return the position of the bubble on-screen when the stack is expanded. + */ + public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) { + final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles); + final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); + final float centerPosition = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + // alignment - centered on the edge + final float rowStart = centerPosition - (expandedStackSize / 2f); + float x; + float y; + if (showBubblesVertically()) { + y = rowStart + positionInRow; + int left = mIsLargeScreen + ? mExpandedViewLargeScreenInset - mExpandedViewPadding - mBubbleSize + : mPositionRect.left; + int right = mIsLargeScreen + ? mPositionRect.right - mExpandedViewLargeScreenInset + mExpandedViewPadding + : mPositionRect.right - mBubbleSize; + x = state.onLeft + ? left + : right; + } else { + y = mPositionRect.top + mExpandedViewPadding; + x = rowStart + positionInRow; + } + + if (showBubblesVertically() && mImeVisible) { + return new PointF(x, getExpandedBubbleYForIme(index, state.numberOfBubbles)); + } + return new PointF(x, y); + } + + /** + * Returns the position of the bubble on-screen when the stack is expanded and the IME + * is showing. + * + * @param index the index of the bubble in the stack. + * @param numberOfBubbles the total number of bubbles in the stack. + * @return y position of the bubble on-screen when the stack is expanded. + */ + private float getExpandedBubbleYForIme(int index, int numberOfBubbles) { + final float top = getAvailableRect().top + mExpandedViewPadding; + if (!showBubblesVertically()) { + // Showing horizontally: align to top + return top; + } + + // Showing vertically: might need to translate the bubbles above the IME. + // Subtract spacing here to provide a margin between top of IME and bottom of bubble row. + final float bottomInset = getImeHeight() + mInsets.bottom - (mSpacingBetweenBubbles * 2); + final float expandedStackSize = getExpandedStackSize(numberOfBubbles); + final float centerPosition = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + final float rowBottom = centerPosition + (expandedStackSize / 2f); + final float rowTop = centerPosition - (expandedStackSize / 2f); + float rowTopForIme = rowTop; + if (rowBottom > bottomInset) { + // We overlap with IME, must shift the bubbles + float translationY = rowBottom - bottomInset; + rowTopForIme = Math.max(rowTop - translationY, top); + if (rowTop - translationY < top) { + // Even if we shift the bubbles, they will still overlap with the IME. + // Hide the overflow for a lil more space: + final float expandedStackSizeNoO = getExpandedStackSize(numberOfBubbles - 1); + final float centerPositionNoO = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f); + final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f); + translationY = rowBottomNoO - bottomInset; + rowTopForIme = rowTopNoO - translationY; + } + } + return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles)); + } + + /** + * @return the width of the bubble flyout (message originating from the bubble). + */ + public float getMaxFlyoutSize() { + if (isLargeScreen()) { + return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN, + mMinimumFlyoutWidthLargeScreen); + } + return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT; + } + + /** + * @return whether the stack is considered on the left side of the screen. + */ + public boolean isStackOnLeft(PointF currentStackPosition) { + if (currentStackPosition == null) { + currentStackPosition = getRestingPosition(); + } + final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2; + return stackCenter < mScreenRect.width() / 2; + } + + /** * Sets the stack's most recent position along the edge of the screen. This is saved when the * last bubble is removed, so that the stack can be restored in its previous position. */ 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 ac97c8f80617..300319a2f78f 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 @@ -19,6 +19,8 @@ package com.android.wm.shell.bubbles; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +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.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -26,6 +28,8 @@ import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RES import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.ContentResolver; @@ -33,11 +37,11 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Outline; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.provider.Settings; import android.util.Log; @@ -106,14 +110,8 @@ public class BubbleStackView extends FrameLayout */ private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; - /** Duration of the flyout alpha animations. */ - private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; - private static final int FADE_IN_DURATION = 320; - /** Percent to darken the bubbles when they're in the dismiss target. */ - private static final float DARKEN_PERCENT = 0.3f; - /** How long to wait, in milliseconds, before hiding the flyout. */ @VisibleForTesting static final int FLYOUT_HIDE_AFTER = 5000; @@ -122,6 +120,8 @@ public class BubbleStackView extends FrameLayout private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; + private static final float SCRIM_ALPHA = 0.6f; + /** * How long to wait to animate the stack temporarily invisible after a drag/flyout hide * animation ends, if we are in fact temporarily invisible. @@ -188,6 +188,7 @@ public class BubbleStackView extends FrameLayout }; private final BubbleController mBubbleController; private final BubbleData mBubbleData; + private StackViewState mStackViewState = new StackViewState(); private final ValueAnimator mDismissBubbleAnimator; @@ -195,7 +196,8 @@ public class BubbleStackView extends FrameLayout private StackAnimationController mStackAnimationController; private ExpandedAnimationController mExpandedAnimationController; - private View mTaskbarScrim; + private View mScrim; + private View mManageMenuScrim; private FrameLayout mExpandedViewContainer; /** Matrix used to scale the expanded view container with a given pivot point. */ @@ -245,7 +247,6 @@ public class BubbleStackView extends FrameLayout private int mBubbleTouchPadding; private int mExpandedViewPadding; private int mCornerRadius; - private int mImeOffset; @Nullable private BubbleViewProvider mExpandedBubble; private boolean mIsExpanded; @@ -555,7 +556,7 @@ public class BubbleStackView extends FrameLayout if (mBubbleData.isExpanded()) { if (mManageEduView != null) { - mManageEduView.hide(false /* show */); + mManageEduView.hide(); } // If we're expanded, tell the animation controller to prepare to drag this bubble, @@ -756,7 +757,6 @@ public class BubbleStackView extends FrameLayout mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); - mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); @@ -777,8 +777,8 @@ public class BubbleStackView extends FrameLayout floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, this::animateShadows /* onStackAnimationFinished */, mPositioner); - mExpandedAnimationController = new ExpandedAnimationController( - mPositioner, mExpandedViewPadding, onBubbleAnimatedOut); + mExpandedAnimationController = new ExpandedAnimationController(mPositioner, + onBubbleAnimatedOut, this); mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or @@ -793,8 +793,6 @@ public class BubbleStackView extends FrameLayout mBubbleContainer.setClipChildren(false); addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - updateUserEdu(); - mExpandedViewContainer = new FrameLayout(context); mExpandedViewContainer.setElevation(elevation); mExpandedViewContainer.setClipChildren(false); @@ -858,11 +856,20 @@ public class BubbleStackView extends FrameLayout mBubbleData.setExpanded(true); }); - mTaskbarScrim = new View(getContext()); - mTaskbarScrim.setBackgroundColor(Color.BLACK); - addView(mTaskbarScrim); - mTaskbarScrim.setAlpha(0f); - mTaskbarScrim.setVisibility(GONE); + mScrim = new View(getContext()); + mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + addView(mScrim); + mScrim.setAlpha(0f); + + mManageMenuScrim = new View(getContext()); + mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mManageMenuScrim.setAlpha(0f); + mManageMenuScrim.setVisibility(INVISIBLE); mOrientationChangedListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { @@ -882,12 +889,15 @@ public class BubbleStackView extends FrameLayout // Re-draw bubble row and pointer for new orientation. beforeExpandedViewAnimation(); updateOverflowVisibility(); - updatePointerPosition(); + updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { afterExpandedViewAnimation(); + showManageMenu(mShowingManage); } /* after */); + final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, + getBubbleIndex(mExpandedBubble)); mExpandedViewContainer.setTranslationX(0f); - mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); + mExpandedViewContainer.setTranslationY(translationY); mExpandedViewContainer.setAlpha(1f); } removeOnLayoutChangeListener(mOrientationChangedListener); @@ -917,8 +927,10 @@ public class BubbleStackView extends FrameLayout setOnClickListener(view -> { if (mShowingManage) { showManageMenu(false /* show */); + } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); } else if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) { - mStackEduView.hide(false); + mStackEduView.hide(false /* isExpanding */); } else if (mBubbleData.isExpanded()) { mBubbleData.setExpanded(false); } @@ -955,8 +967,9 @@ public class BubbleStackView extends FrameLayout } }); mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { - if (mExpandedBubble != null) { - mExpandedBubble.setExpandedContentAlpha((float) valueAnimator.getAnimatedValue()); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setTaskViewAlpha( + (float) valueAnimator.getAnimatedValue()); } }); @@ -1117,10 +1130,10 @@ public class BubbleStackView extends FrameLayout return; } if (mManageEduView == null) { - mManageEduView = new ManageEducationView(mContext); + mManageEduView = new ManageEducationView(mContext, mPositioner); addView(mManageEduView); } - mManageEduView.show(mExpandedBubble.getExpandedView(), mTempRect); + mManageEduView.show(mExpandedBubble.getExpandedView()); } /** @@ -1148,21 +1161,27 @@ public class BubbleStackView extends FrameLayout return false; } if (mStackEduView == null) { - mStackEduView = new StackEducationView(mContext); + mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); addView(mStackEduView); } mBubbleContainer.bringToFront(); return mStackEduView.show(mPositioner.getDefaultStartPosition()); } + // Recreates & shows the education views. Call when a theme/config change happens. private void updateUserEdu() { - maybeShowStackEdu(); - if (mManageEduView != null) { - mManageEduView.invalidate(); + if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) { + removeView(mStackEduView); + mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); + addView(mStackEduView); + mBubbleContainer.bringToFront(); // Stack appears on top of the stack education + mStackEduView.show(mPositioner.getDefaultStartPosition()); } - maybeShowManageEdu(); - if (mStackEduView != null) { - mStackEduView.invalidate(); + if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + removeView(mManageEduView); + mManageEduView = new ManageEducationView(mContext, mPositioner); + addView(mManageEduView); + mManageEduView.show(mExpandedBubble.getExpandedView()); } } @@ -1171,7 +1190,7 @@ public class BubbleStackView extends FrameLayout if (mFlyout != null) { removeView(mFlyout); } - mFlyout = new BubbleFlyoutView(getContext()); + mFlyout = new BubbleFlyoutView(getContext(), mPositioner); mFlyout.setVisibility(GONE); mFlyout.setOnClickListener(mFlyoutClickListener); mFlyout.setOnTouchListener(mFlyoutTouchListener); @@ -1218,6 +1237,10 @@ public class BubbleStackView extends FrameLayout updateOverflow(); updateUserEdu(); updateExpandedViewTheme(); + mScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); } /** @@ -1229,9 +1252,6 @@ public class BubbleStackView extends FrameLayout mRelativeStackPositionBeforeRotation = new RelativeStackPosition( mPositioner.getRestingPosition(), mStackAnimationController.getAllowableStackPositionRegion()); - mManageMenu.setVisibility(View.INVISIBLE); - mShowingManage = false; - addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); } @@ -1255,6 +1275,7 @@ public class BubbleStackView extends FrameLayout setUpManageMenu(); setUpFlyout(); setUpDismissView(); + updateUserEdu(); mBubbleSize = mPositioner.getBubbleSize(); for (Bubble b : mBubbleData.getBubbles()) { if (b.getIconView() == null) { @@ -1292,6 +1313,7 @@ public class BubbleStackView extends FrameLayout @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + mPositioner.update(); getViewTreeObserver().addOnComputeInternalInsetsListener(this); getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); } @@ -1534,7 +1556,8 @@ public class BubbleStackView extends FrameLayout } else { bubble.cleanupViews(); } - updatePointerPosition(); + updatePointerPosition(false /* forIme */); + updateExpandedView(); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } @@ -1574,7 +1597,7 @@ public class BubbleStackView extends FrameLayout .map(b -> b.getIconView()).collect(Collectors.toList()); mStackAnimationController.animateReorder(bubbleViews, reorder); } - updatePointerPosition(); + updatePointerPosition(false /* forIme */); } /** @@ -1645,7 +1668,6 @@ public class BubbleStackView extends FrameLayout private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; - updatePointerPosition(); if (mIsExpanded) { hideCurrentInputMethod(); @@ -1710,6 +1732,21 @@ public class BubbleStackView extends FrameLayout notifyExpansionChanged(mExpandedBubble, mIsExpanded); } + /** + * Called when back press occurs while bubbles are expanded. + */ + public void onBackPressed() { + if (mIsExpanded) { + if (mShowingManage) { + showManageMenu(false); + } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); + } else { + setExpanded(false); + } + } + } + void setBubbleVisibility(Bubble b, boolean visible) { if (b.getIconView() != null) { b.getIconView().setVisibility(visible ? VISIBLE : GONE); @@ -1722,6 +1759,7 @@ public class BubbleStackView extends FrameLayout * not. */ void hideCurrentInputMethod() { + mPositioner.setImeVisible(false, 0); mBubbleController.hideCurrentInputMethod(); } @@ -1796,6 +1834,20 @@ public class BubbleStackView extends FrameLayout mExpandedViewAlphaAnimator.start(); } + private void showScrim(boolean show) { + if (show) { + mScrim.animate() + .setInterpolator(ALPHA_IN) + .alpha(SCRIM_ALPHA) + .start(); + } else { + mScrim.animate() + .alpha(0f) + .setInterpolator(ALPHA_OUT) + .start(); + } + } + private void animateExpansion() { cancelDelayedExpandCollapseSwitchAnimations(); final boolean showVertically = mPositioner.showBubblesVertically(); @@ -1805,47 +1857,38 @@ public class BubbleStackView extends FrameLayout } beforeExpandedViewAnimation(); + showScrim(true); updateZOrder(); updateBadges(false /* setBadgeForCollapsedStack */); mBubbleContainer.setActiveController(mExpandedAnimationController); updateOverflowVisibility(); - updatePointerPosition(); + updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { maybeShowManageEdu(); } } /* after */); - - if (mPositioner.showingInTaskbar() - // Don't need the scrim when the bar is at the bottom - && mPositioner.getTaskbarPosition() != BubblePositioner.TASKBAR_POSITION_BOTTOM) { - mTaskbarScrim.getLayoutParams().width = mPositioner.getTaskbarSize(); - mTaskbarScrim.setTranslationX(mStackOnLeftOrWillBe - ? 0f - : mPositioner.getAvailableRect().right - mPositioner.getTaskbarSize()); - mTaskbarScrim.setVisibility(VISIBLE); - mTaskbarScrim.animate().alpha(1f).start(); - } - - mExpandedViewContainer.setTranslationX(0f); - mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); - mExpandedViewContainer.setAlpha(1f); - int index; if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { index = mBubbleData.getBubbles().size(); } else { index = getBubbleIndex(mExpandedBubble); } - // Position of the bubble we're expanding, once it's settled in its row. - final float bubbleWillBeAt = - mExpandedAnimationController.getBubbleXOrYForOrientation(index); + PointF p = mPositioner.getExpandedBubbleXY(index, getState()); + final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? p.y : p.x); + mExpandedViewContainer.setTranslationX(0f); + mExpandedViewContainer.setTranslationY(translationY); + mExpandedViewContainer.setAlpha(1f); // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles // that are animating farther, so that the expanded view doesn't move as much. final float relevantStackPosition = showVertically ? mStackAnimationController.getStackPosition().y : mStackAnimationController.getStackPosition().x; + final float bubbleWillBeAt = showVertically + ? p.y + : p.x; final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); // Wait for the path animation target to reach its end, and add a small amount of extra time @@ -1862,27 +1905,27 @@ public class BubbleStackView extends FrameLayout // Set the pivot point for the scale, so the expanded view animates out from the bubble. if (showVertically) { float pivotX; - float pivotY = bubbleWillBeAt + mBubbleSize / 2f; if (mStackOnLeftOrWillBe) { - pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + pivotX = p.x + mBubbleSize + mExpandedViewPadding; } else { - pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; + pivotX = p.x - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - pivotX, pivotY); + pivotX, + p.y + mBubbleSize / 2f); } else { mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - bubbleWillBeAt + mBubbleSize / 2f, - mPositioner.getExpandedViewY()); + p.x + mBubbleSize / 2f, + p.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); if (mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.setExpandedContentAlpha(0f); + mExpandedBubble.getExpandedView().setTaskViewAlpha(0f); // We'll be starting the alpha animation after a slight delay, so set this flag early // here. @@ -1914,6 +1957,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainerMatrix); }) .withEndActions(() -> { + mExpandedViewContainer.setAnimationMatrix(null); afterExpandedViewAnimation(); if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { @@ -1929,12 +1973,17 @@ public class BubbleStackView extends FrameLayout private void animateCollapse() { cancelDelayedExpandCollapseSwitchAnimations(); + if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); + } // Hide the menu if it's visible. showManageMenu(false); mIsExpanded = false; mIsExpansionAnimating = true; + showScrim(false); + mBubbleContainer.cancelAllAnimations(); // If we were in the middle of swapping, the animating-out surface would have been scaling @@ -1952,10 +2001,6 @@ public class BubbleStackView extends FrameLayout /* collapseTo */, () -> mBubbleContainer.setActiveController(mStackAnimationController)); - if (mTaskbarScrim.getVisibility() == VISIBLE) { - mTaskbarScrim.animate().alpha(0f).start(); - } - int index; if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { index = mBubbleData.getBubbles().size(); @@ -1963,12 +2008,10 @@ public class BubbleStackView extends FrameLayout index = mBubbleData.getBubbles().indexOf(mExpandedBubble); } // Value the bubble is animating from (back into the stack). - final float expandingFromBubbleAt = - mExpandedAnimationController.getBubbleXOrYForOrientation(index); - final boolean showVertically = mPositioner.showBubblesVertically(); + final PointF p = mPositioner.getExpandedBubbleXY(index, getState()); if (mPositioner.showBubblesVertically()) { float pivotX; - float pivotY = expandingFromBubbleAt + mBubbleSize / 2f; + float pivotY = p.y + mBubbleSize / 2f; if (mStackOnLeftOrWillBe) { pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; } else { @@ -1980,8 +2023,8 @@ public class BubbleStackView extends FrameLayout } else { mExpandedViewContainerMatrix.setScale( 1f, 1f, - expandingFromBubbleAt + mBubbleSize / 2f, - mPositioner.getExpandedViewY()); + p.x + mBubbleSize / 2f, + p.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewAlphaAnimator.reverse(); @@ -2008,7 +2051,7 @@ public class BubbleStackView extends FrameLayout final BubbleViewProvider previouslySelected = mExpandedBubble; beforeExpandedViewAnimation(); if (mManageEduView != null) { - mManageEduView.hide(false /* fromExpansion */); + mManageEduView.hide(); } if (DEBUG_BUBBLE_STACK_VIEW) { @@ -2023,10 +2066,6 @@ public class BubbleStackView extends FrameLayout if (previouslySelected != null) { previouslySelected.setTaskViewVisibility(false); } - - if (mPositioner.showingInTaskbar()) { - mTaskbarScrim.setVisibility(GONE); - } }) .start(); } @@ -2063,32 +2102,31 @@ public class BubbleStackView extends FrameLayout boolean isOverflow = mExpandedBubble != null && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); - float expandingFromBubbleDestination = - mExpandedAnimationController.getBubbleXOrYForOrientation(isOverflow - ? getBubbleCount() - : mBubbleData.getBubbles().indexOf(mExpandedBubble)); - + PointF p = mPositioner.getExpandedBubbleXY(isOverflow + ? mBubbleContainer.getChildCount() - 1 + : mBubbleData.getBubbles().indexOf(mExpandedBubble), + getState()); mExpandedViewContainer.setAlpha(1f); mExpandedViewContainer.setVisibility(View.VISIBLE); if (mPositioner.showBubblesVertically()) { float pivotX; - float pivotY = expandingFromBubbleDestination + mBubbleSize / 2f; + float pivotY = p.y + mBubbleSize / 2f; if (mStackOnLeftOrWillBe) { - pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + pivotX = p.x + mBubbleSize + mExpandedViewPadding; } else { - pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; - + pivotX = p.x - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( - 0f, 0f, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, pivotX, pivotY); } else { mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - expandingFromBubbleDestination + mBubbleSize / 2f, - mPositioner.getExpandedViewY()); + p.x + mBubbleSize / 2f, + p.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -2113,6 +2151,7 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { mExpandedViewTemporarilyHidden = false; mIsBubbleSwitchAnimating = false; + mExpandedViewContainer.setAnimationMatrix(null); }) .start(); }, 25); @@ -2144,9 +2183,20 @@ public class BubbleStackView extends FrameLayout } } - /** Moves the bubbles out of the way if they're going to be over the keyboard. */ - public void onImeVisibilityChanged(boolean visible, int height) { - mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); + /** + * Updates the stack based for IME changes. When collapsed it'll move the stack if it + * overlaps where they IME would be. When expanded it'll shift the expanded bubbles + * if they might overlap with the IME (this only happens for large screens). + */ + public void animateForIme(boolean visible) { + if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) { + // This will update the animation so the bubbles move to position for the IME + mExpandedAnimationController.expandFromStack(() -> { + updatePointerPosition(false /* forIme */); + afterExpandedViewAnimation(); + } /* after */); + return; + } if (!mIsExpanded && getBubbleCount() > 0) { final float stackDestinationY = @@ -2165,9 +2215,20 @@ public class BubbleStackView extends FrameLayout FLYOUT_IME_ANIMATION_SPRING_CONFIG) .start(); } - } else if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { + } else if (mPositioner.showBubblesVertically() && mIsExpanded + && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { mExpandedBubble.getExpandedView().setImeVisible(visible); + List<Animator> animList = new ArrayList(); + for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { + View child = mBubbleContainer.getChildAt(i); + float transY = mPositioner.getExpandedBubbleXY(i, getState()).y; + ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY); + animList.add(anim); + } + updatePointerPosition(true /* forIme */); + AnimatorSet set = new AnimatorSet(); + set.playTogether(animList); + set.start(); } } @@ -2403,20 +2464,19 @@ public class BubbleStackView extends FrameLayout if (mFlyout.getVisibility() == View.VISIBLE) { - mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(), + mFlyout.animateUpdate(bubble.getFlyoutMessage(), mStackAnimationController.getStackPosition(), !bubble.showDot(), mAfterFlyoutHidden /* onHide */); } else { mFlyout.setVisibility(INVISIBLE); mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), - mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.getStackPosition(), mStackAnimationController.isStackOnLeftSide(), bubble.getIconView().getDotColor() /* dotColor */, expandFlyoutAfterDelay /* onLayoutComplete */, mAfterFlyoutHidden /* onHide */, bubble.getIconView().getDotCenter(), - !bubble.showDot(), - mPositioner); + !bubble.showDot()); } mFlyout.bringToFront(); }); @@ -2472,7 +2532,7 @@ public class BubbleStackView extends FrameLayout // Account for the IME in the touchable region so that the touchable region of the // Bubble window doesn't obscure the IME. The touchable region affects which areas // of the screen can be excluded by lower windows (IME is just above the embedded task) - outRect.bottom -= (int) mStackAnimationController.getImeHeight(); + outRect.bottom -= mPositioner.getImeHeight(); } if (mFlyout.getVisibility() == View.VISIBLE) { @@ -2491,15 +2551,36 @@ public class BubbleStackView extends FrameLayout invalidate(); } - private void showManageMenu(boolean show) { + /** Hide or show the manage menu for the currently expanded bubble. */ + @VisibleForTesting + public void showManageMenu(boolean show) { mShowingManage = show; // This should not happen, since the manage menu is only visible when there's an expanded // bubble. If we end up in this state, just hide the menu immediately. if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { mManageMenu.setVisibility(View.INVISIBLE); + mManageMenuScrim.setVisibility(INVISIBLE); + mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */); return; } + if (show) { + mManageMenuScrim.setVisibility(VISIBLE); + mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f); + } + Runnable endAction = () -> { + if (!show) { + mManageMenuScrim.setVisibility(INVISIBLE); + mManageMenuScrim.setTranslationZ(0f); + } + }; + + mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show); + mManageMenuScrim.animate() + .setInterpolator(show ? ALPHA_IN : ALPHA_OUT) + .alpha(show ? SCRIM_ALPHA : 0f) + .withEndAction(endAction) + .start(); // If available, update the manage menu's settings option with the expanded bubble's app // name and icon. @@ -2510,7 +2591,6 @@ public class BubbleStackView extends FrameLayout R.string.bubbles_app_settings, bubble.getAppName())); } - mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); if (mExpandedBubble.getExpandedView().getTaskView() != null) { mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage ? new Rect(0, 0, getWidth(), getHeight()) @@ -2522,7 +2602,11 @@ public class BubbleStackView extends FrameLayout // When the menu is open, it should be at these coordinates. The menu pops out to the right // in LTR and to the left in RTL. - final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth(); + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); + final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin(); + final float targetX = isLtr + ? mTempRect.left - margin + : mTempRect.right + margin - mManageMenu.getWidth(); final float targetY = mTempRect.bottom - mManageMenu.getHeight(); final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; @@ -2702,18 +2786,21 @@ public class BubbleStackView extends FrameLayout } boolean isOverflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()); - int[] paddings = mPositioner.getExpandedViewPadding( + int[] paddings = mPositioner.getExpandedViewContainerPadding( mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded); mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]); if (mIsExpansionAnimating) { mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); } if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); + PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), + getState()); + mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? p.y : p.x)); mExpandedViewContainer.setTranslationX(0f); mExpandedBubble.getExpandedView().updateView( mExpandedViewContainer.getLocationOnScreen()); - updatePointerPosition(); + updatePointerPosition(false /* forIme */); } mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); @@ -2784,7 +2871,13 @@ public class BubbleStackView extends FrameLayout } } - private void updatePointerPosition() { + /** + * Updates the position of the pointer based on the expanded bubble. + * + * @param forIme whether the position is being updated due to the ime appearing, in this case + * the pointer is animated to the location. + */ + private void updatePointerPosition(boolean forIme) { if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { return; } @@ -2792,8 +2885,12 @@ public class BubbleStackView extends FrameLayout if (index == -1) { return; } - float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index); - mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, mStackOnLeftOrWillBe); + PointF position = mPositioner.getExpandedBubbleXY(index, getState()); + float bubblePosition = mPositioner.showBubblesVertically() + ? position.y + : position.x; + mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, + mStackOnLeftOrWillBe, forIme /* animate */); } /** @@ -2876,6 +2973,26 @@ public class BubbleStackView extends FrameLayout return bubbles; } + /** @return the current stack state. */ + public StackViewState getState() { + mStackViewState.numberOfBubbles = mBubbleContainer.getChildCount(); + mStackViewState.selectedIndex = getBubbleIndex(mExpandedBubble); + mStackViewState.onLeft = mStackOnLeftOrWillBe; + return mStackViewState; + } + + /** + * Holds some commonly queried information about the stack. + */ + public static class StackViewState { + // Number of bubbles (including the overflow itself) in the stack. + public int numberOfBubbles; + // The selected index if the stack is expanded. + public int selectedIndex; + // Whether the stack is resting on the left or right side of the screen when collapsed. + public boolean onLeft; + } + /** * Representation of stack position that uses relative properties rather than absolute * coordinates. This is used to maintain similar stack positions across configuration changes. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java index 38b3ba9dfda0..7e552826e94a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java @@ -29,12 +29,6 @@ public interface BubbleViewProvider { @Nullable BubbleExpandedView getExpandedView(); /** - * Sets the alpha of the expanded view content. This will be applied to both the expanded view - * container itself (the manage button, etc.) as well as the TaskView within it. - */ - void setExpandedContentAlpha(float alpha); - - /** * Sets whether the contents of the bubble's TaskView should be visible. */ void setTaskViewVisibility(boolean visible); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index c73b5eebc5c2..c82249b8a369 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -24,12 +24,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.os.Bundle; -import android.os.Looper; import android.service.notification.NotificationListenerService.RankingMap; import android.util.ArraySet; import android.util.Pair; import android.util.SparseArray; -import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -43,7 +41,6 @@ import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; import java.util.concurrent.Executor; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -160,14 +157,6 @@ public interface Bubbles { /** Set the proxy to commnuicate with SysUi side components. */ void setSysuiProxy(SysuiProxy proxy); - /** - * Set the scrim view for bubbles. - * - * @param callback The callback made with the executor and the executor's looper that the view - * will be running on. - **/ - void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback); - /** Set a listener to be notified of bubble expand events. */ void setExpandListener(BubbleExpandListener listener); @@ -295,6 +284,8 @@ public interface Bubbles { void onStackExpandChanged(boolean shouldExpand); + void onManageMenuExpandChanged(boolean menuExpanded); + void onUnbubbleConversation(String key); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt index 0a1cd2246339..74672a336161 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt @@ -28,37 +28,40 @@ import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW import com.android.wm.shell.R import com.android.wm.shell.animation.PhysicsAnimator import com.android.wm.shell.common.DismissCircleView +import android.view.WindowInsets +import android.view.WindowManager /* * View that handles interactions between DismissCircleView and BubbleStackView. */ class DismissView(context: Context) : FrameLayout(context) { - var circle = DismissCircleView(context).apply { - val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) - val newParams = LayoutParams(targetSize, targetSize) - newParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - setLayoutParams(newParams) - setTranslationY( - resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height).toFloat()) - } - + var circle = DismissCircleView(context) var isShowing = false + private val animator = PhysicsAnimator.getInstance(circle) private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) private val DISMISS_SCRIM_FADE_MS = 200 + private var wm: WindowManager = + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager init { setLayoutParams(LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height), Gravity.BOTTOM)) - setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) + updatePadding() setClipToPadding(false) setClipChildren(false) setVisibility(View.INVISIBLE) setBackgroundResource( R.drawable.floating_dismiss_gradient_transition) - addView(circle) + + val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + addView(circle, LayoutParams(targetSize, targetSize, + Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)) + // start with circle offscreen so it's animated up + circle.setTranslationY(resources.getDimensionPixelSize( + R.dimen.floating_dismiss_gradient_height).toFloat()) } /** @@ -91,9 +94,21 @@ class DismissView(context: Context) : FrameLayout(context) { } fun updateResources() { - val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + updatePadding() + layoutParams.height = resources.getDimensionPixelSize( + R.dimen.floating_dismiss_gradient_height) + + val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) circle.layoutParams.width = targetSize circle.layoutParams.height = targetSize circle.requestLayout() } + + private fun updatePadding() { + val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets() + val navInset = insets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars()) + setPadding(0, 0, 0, navInset.bottom + + resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) + } }
\ No newline at end of file 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 4cc67025fff4..eb4737ac6c63 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 @@ -18,12 +18,13 @@ package com.android.wm.shell.bubbles import android.content.Context import android.graphics.Color import android.graphics.Rect +import android.graphics.drawable.ColorDrawable import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout -import android.widget.TextView -import com.android.internal.util.ContrastColorUtil +import com.android.internal.R.color.system_neutral1_900 import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators @@ -31,21 +32,22 @@ 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 constructor(context: Context) : LinearLayout(context) { +class ManageEducationView constructor(context: Context, positioner: BubblePositioner) + : LinearLayout(context) { - private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView" + private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView" else BubbleDebugConfig.TAG_BUBBLES private val ANIMATE_DURATION: Long = 200 - private val ANIMATE_DURATION_SHORT: Long = 40 - private val manageView by lazy { findViewById<View>(R.id.manage_education_view) } - private val manageButton by lazy { findViewById<Button>(R.id.manage) } + private val positioner: BubblePositioner = positioner + private val manageView by lazy { findViewById<ViewGroup>(R.id.manage_education_view) } + private val manageButton by lazy { findViewById<Button>(R.id.manage_button) } private val gotItButton by lazy { findViewById<Button>(R.id.got_it) } - private val titleTextView by lazy { findViewById<TextView>(R.id.user_education_title) } - private val descTextView by lazy { findViewById<TextView>(R.id.user_education_description) } private var isHiding = false + private var realManageButtonRect = Rect() + private var bubbleExpandedView: BubbleExpandedView? = null init { LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this) @@ -66,18 +68,17 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) override fun onFinishInflate() { super.onFinishInflate() layoutDirection = resources.configuration.layoutDirection - setTextColor() } - private fun setTextColor() { - val typedArray = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, - android.R.attr.textColorPrimaryInverse)) - val bgColor = typedArray.getColor(0 /* index */, Color.BLACK) - var textColor = typedArray.getColor(1 /* index */, Color.WHITE) + private fun setButtonColor() { + val typedArray = mContext.obtainStyledAttributes(intArrayOf( + com.android.internal.R.attr.colorAccentPrimary)) + val buttonColor = typedArray.getColor(0 /* index */, Color.TRANSPARENT) typedArray.recycle() - textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) - titleTextView.setTextColor(textColor) - descTextView.setTextColor(textColor) + + manageButton.setTextColor(mContext.getColor(system_neutral1_900)) + manageButton.setBackgroundDrawable(ColorDrawable(buttonColor)) + gotItButton.setBackgroundDrawable(ColorDrawable(buttonColor)) } private fun setDrawableDirection() { @@ -91,30 +92,39 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) * If necessary, toggles the user education view for the manage button. This is shown when the * bubble stack is expanded for the first time. * - * @param show whether the user education view should show or not. + * @param expandedView the expandedView the user education is shown on top of. */ - fun show(expandedView: BubbleExpandedView, rect: Rect) { + fun show(expandedView: BubbleExpandedView) { + setButtonColor() if (visibility == VISIBLE) return + bubbleExpandedView = expandedView + expandedView.taskView?.setObscuredTouchRect(Rect(positioner.screenRect)) + + layoutParams.width = if (positioner.isLargeScreen) + context.resources.getDimensionPixelSize( + R.dimen.bubbles_user_education_width_large_screen) + else ViewGroup.LayoutParams.MATCH_PARENT + alpha = 0f visibility = View.VISIBLE + expandedView.getManageButtonBoundsOnScreen(realManageButtonRect) + manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin, + manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom) post { - expandedView.getManageButtonBoundsOnScreen(rect) - manageButton .setOnClickListener { - expandedView.findViewById<View>(R.id.settings_button).performClick() - hide(true /* isStackExpanding */) + hide() + expandedView.findViewById<View>(R.id.manage_button).performClick() } - gotItButton.setOnClickListener { hide(true /* isStackExpanding */) } - setOnClickListener { hide(true /* isStackExpanding */) } - - with(manageView) { - translationX = 0f - val inset = resources.getDimensionPixelSize( - R.dimen.bubbles_manage_education_top_inset) - translationY = (rect.top - manageView.height + inset).toFloat() - } + gotItButton.setOnClickListener { hide() } + setOnClickListener { hide() } + + val offsetViewBounds = Rect() + manageButton.getDrawingRect(offsetViewBounds) + manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds) + translationX = 0f + translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat() bringToFront() animate() .setDuration(ANIMATE_DURATION) @@ -124,13 +134,14 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) setShouldShow(false) } - fun hide(isStackExpanding: Boolean) { + fun hide() { + bubbleExpandedView?.taskView?.setObscuredTouchRect(null) if (visibility != VISIBLE || isHiding) return animate() .withStartAction { isHiding = true } .alpha(0f) - .setDuration(if (isStackExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) + .setDuration(ANIMATE_DURATION) .withEndAction { isHiding = false visibility = GONE 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 0a2cfc4089ed..f6a90b7a76cd 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 @@ -18,8 +18,11 @@ package com.android.wm.shell.bubbles import android.content.Context import android.graphics.Color import android.graphics.PointF +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View +import android.view.View.OnKeyListener +import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import com.android.internal.util.ContrastColorUtil @@ -30,7 +33,12 @@ import com.android.wm.shell.animation.Interpolators * User education view to highlight the collapsed stack of bubbles. * Shown only the first time a user taps the stack. */ -class StackEducationView constructor(context: Context) : LinearLayout(context) { +class StackEducationView constructor( + context: Context, + positioner: BubblePositioner, + controller: BubbleController +) + : LinearLayout(context) { private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" else BubbleDebugConfig.TAG_BUBBLES @@ -38,6 +46,9 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { private val ANIMATE_DURATION: Long = 200 private val ANIMATE_DURATION_SHORT: Long = 40 + private val positioner: BubblePositioner = positioner + private val controller: BubbleController = controller + private val view by lazy { findViewById<View>(R.id.stack_education_layout) } private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) } @@ -67,6 +78,28 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { setTextColor() } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + setFocusableInTouchMode(true) + setOnKeyListener(object : OnKeyListener { + override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean { + // if the event is a key down event on the enter button + if (event.action == KeyEvent.ACTION_UP && + keyCode == KeyEvent.KEYCODE_BACK && !isHiding) { + hide(false) + return true + } + return false + } + }) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + setOnKeyListener(null) + controller.updateWindowFlagsForBackpress(false /* interceptBack */) + } + private fun setTextColor() { val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, android.R.attr.textColorPrimaryInverse)) @@ -94,13 +127,25 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { fun show(stackPosition: PointF): Boolean { if (visibility == VISIBLE) return false + controller.updateWindowFlagsForBackpress(true /* interceptBack */) + layoutParams.width = if (positioner.isLargeScreen) + context.resources.getDimensionPixelSize( + R.dimen.bubbles_user_education_width_large_screen) + else ViewGroup.LayoutParams.MATCH_PARENT + setAlpha(0f) setVisibility(View.VISIBLE) post { + requestFocus() with(view) { - val bubbleSize = context.resources.getDimensionPixelSize( - R.dimen.bubble_size) - translationY = stackPosition.y + bubbleSize / 2 - getHeight() / 2 + if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight, + paddingBottom) + } else { + setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft, + paddingBottom) + } + translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 } animate() .setDuration(ANIMATE_DURATION) @@ -114,15 +159,16 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { /** * If necessary, hides the stack education view. * - * @param fromExpansion if true this indicates the hide is happening due to the bubble being + * @param isExpanding if true this indicates the hide is happening due to the bubble being * expanded, false if due to a touch outside of the bubble stack. */ - fun hide(fromExpansion: Boolean) { + fun hide(isExpanding: Boolean) { if (visibility != VISIBLE || isHiding) return + controller.updateWindowFlagsForBackpress(false /* interceptBack */) animate() .alpha(0f) - .setDuration(if (fromExpansion) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) + .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) .withEndAction { visibility = GONE } } 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 df2b440c19df..f0f78748e343 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 @@ -21,7 +21,6 @@ import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RES import android.content.res.Resources; import android.graphics.Path; import android.graphics.PointF; -import android.graphics.Rect; import android.view.View; import androidx.annotation.NonNull; @@ -33,6 +32,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; @@ -64,9 +64,6 @@ public class ExpandedAnimationController /** Stiffness for the expand/collapse path-following animation. */ private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; - /** What percentage of the screen to use when centering the bubbles in landscape. */ - private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f; - /** * Velocity required to dismiss an individual bubble without dragging it into the dismiss * target. @@ -79,16 +76,8 @@ public class ExpandedAnimationController /** Horizontal offset between bubbles, which we need to know to re-stack them. */ private float mStackOffsetPx; - /** Space between status bar and bubbles in the expanded state. */ - private float mBubblePaddingTop; /** Size of each bubble. */ private float mBubbleSizePx; - /** Max number of bubbles shown in row above expanded view. */ - private int mBubblesMaxRendered; - /** Max amount of space to have between bubbles when expanded. */ - private int mBubblesMaxSpace; - /** Amount of space between the bubbles when expanded. */ - private float mSpaceBetweenBubbles; /** Whether the expand / collapse animation is running. */ private boolean mAnimatingExpand = false; @@ -127,8 +116,6 @@ public class ExpandedAnimationController /** The bubble currently being dragged out of the row (to potentially be dismissed). */ private MagnetizedObject<View> mMagnetizedBubbleDraggingOut; - private int mExpandedViewPadding; - /** * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the * end of this animation means we have no bubbles left, and notify the BubbleController. @@ -137,13 +124,15 @@ public class ExpandedAnimationController private BubblePositioner mPositioner; - public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding, - Runnable onBubbleAnimatedOutAction) { + private BubbleStackView mBubbleStackView; + + public ExpandedAnimationController(BubblePositioner positioner, + Runnable onBubbleAnimatedOutAction, BubbleStackView stackView) { mPositioner = positioner; updateResources(); - mExpandedViewPadding = expandedViewPadding; mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; mCollapsePoint = mPositioner.getDefaultStartPosition(); + mBubbleStackView = stackView; } /** @@ -208,11 +197,8 @@ public class ExpandedAnimationController return; } Resources res = mLayout.getContext().getResources(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); mBubbleSizePx = mPositioner.getBubbleSize(); - mBubblesMaxRendered = mPositioner.getMaxBubbles(); - mSpaceBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); } /** @@ -256,31 +242,19 @@ public class ExpandedAnimationController final Path path = new Path(); path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); - final float expandedY = mPositioner.showBubblesVertically() - ? getBubbleXOrYForOrientation(index) - : getExpandedY(); + final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); if (expanding) { - // If we're expanding, first draw a line from the bubble's current position to the - // top of the screen. - path.lineTo(bubble.getTranslationX(), expandedY); + // If we're expanding, first draw a line from the bubble's current position to where + // it'll end up + path.lineTo(bubble.getTranslationX(), p.y); // Then, draw a line across the screen to the bubble's resting position. - if (mPositioner.showBubblesVertically()) { - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); - float translationX = onLeft - ? availableRect.left - : availableRect.right - mBubbleSizePx; - path.lineTo(translationX, getBubbleXOrYForOrientation(index)); - } else { - path.lineTo(getBubbleXOrYForOrientation(index), expandedY); - } + path.lineTo(p.x, p.y); } else { final float stackedX = mCollapsePoint.x; // If we're collapsing, draw a line from the bubble's current position to the side // of the screen where the bubble will be stacked. - path.lineTo(stackedX, expandedY); + path.lineTo(stackedX, p.y); // Then, draw a line down to the stack position. path.lineTo(stackedX, mCollapsePoint.y @@ -390,8 +364,9 @@ public class ExpandedAnimationController bubbleView.setTranslationY(y); } + final float expandedY = mPositioner.getExpandedViewYTopAligned(); final boolean draggedOutEnough = - y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; + y > expandedY + mBubbleSizePx || y < expandedY - mBubbleSizePx; if (draggedOutEnough != mBubbleDraggedOutEnough) { updateBubblePositions(); mBubbleDraggedOutEnough = draggedOutEnough; @@ -435,9 +410,9 @@ public class ExpandedAnimationController return; } final int index = mLayout.indexOfChild(bubbleView); - + final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); animationForChildAtIndex(index) - .position(getBubbleXOrYForOrientation(index), getExpandedY()) + .position(p.x, p.y) .withPositionStartVelocities(velX, velY) .start(() -> bubbleView.setTranslationZ(0f) /* after */); @@ -453,20 +428,6 @@ public class ExpandedAnimationController updateBubblePositions(); } - /** - * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. - */ - public void updateYPosition(Runnable after) { - if (mLayout == null) return; - animationsForChildrenFromIndex( - 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); - } - - /** The Y value of the row of expanded bubbles. */ - public float getExpandedY() { - return mPositioner.getAvailableRect().top + mBubblePaddingTop; - } - /** Description of current animation controller state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("ExpandedAnimationController state:"); @@ -522,37 +483,35 @@ public class ExpandedAnimationController startOrUpdatePathAnimation(true /* expanding */); } else if (mAnimatingCollapse) { startOrUpdatePathAnimation(false /* expanding */); - } else if (mPositioner.showBubblesVertically()) { - child.setTranslationY(getBubbleXOrYForOrientation(index)); - if (!mPreparingToCollapse) { - // Only animate if we're not collapsing as that animation will handle placing the + } else { + boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint); + final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); + if (mPositioner.showBubblesVertically()) { + child.setTranslationY(p.y); + } else { + child.setTranslationX(p.x); + } + + if (mPreparingToCollapse) { + // Don't animate if we're collapsing, as that animation will handle placing the // new bubble in the stacked position. - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); + return; + } + + if (mPositioner.showBubblesVertically()) { float fromX = onLeft - ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR - : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; - float toX = onLeft - ? availableRect.left + mExpandedViewPadding - : availableRect.right - mBubbleSizePx - mExpandedViewPadding; + ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR + : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationX(fromX, toX) + .translationX(fromX, p.y) .start(); - updateBubblePositions(); - } - } else { - child.setTranslationX(getBubbleXOrYForOrientation(index)); - if (!mPreparingToCollapse) { - // Only animate if we're not collapsing as that animation will handle placing the - // new bubble in the stacked position. - float toY = getExpandedY(); - float fromY = getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; + } else { + float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationY(fromY, toY) + .translationY(fromY, p.y) .start(); - updateBubblePositions(); } + updateBubblePositions(); } } @@ -599,7 +558,6 @@ public class ExpandedAnimationController if (mAnimatingExpand || mAnimatingCollapse) { return; } - for (int i = 0; i < mLayout.getChildCount(); i++) { final View bubble = mLayout.getChildAt(i); @@ -609,49 +567,11 @@ public class ExpandedAnimationController return; } - if (mPositioner.showBubblesVertically()) { - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); - animationForChild(bubble) - .translationX(onLeft - ? availableRect.left - : availableRect.right - mBubbleSizePx) - .translationY(getBubbleXOrYForOrientation(i)) - .start(); - } else { - animationForChild(bubble) - .translationX(getBubbleXOrYForOrientation(i)) - .translationY(getExpandedY()) - .start(); - } - } - } - - // TODO - could move to method on bubblePositioner if mSpaceBetweenBubbles gets moved - /** - * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal - * row. When in landscape or on a large screen, they show at the left or right side in a - * vertical row. This method accounts for screen orientation and will return an x or y value - * for the position of the bubble in the row. - * - * @param index Bubble index in row. - * @return the y position of the bubble if showing vertically and the x position if showing - * horizontally. - */ - public float getBubbleXOrYForOrientation(int index) { - if (mLayout == null) { - return 0; + final PointF p = mPositioner.getExpandedBubbleXY(i, mBubbleStackView.getState()); + animationForChild(bubble) + .translationX(p.x) + .translationY(p.y) + .start(); } - final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles); - Rect availableRect = mPositioner.getAvailableRect(); - final boolean isLandscape = mPositioner.showBubblesVertically(); - final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx) - + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles); - final float centerPosition = isLandscape - ? availableRect.centerY() - : availableRect.centerX(); - final float rowStart = centerPosition - (expandedStackSize / 2f); - return rowStart + positionInBar; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 636e1452aa9b..60b64333114e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -127,9 +127,6 @@ public class StackAnimationController extends /** Whether or not the stack's start position has been set. */ private boolean mStackMovedToStartPosition = false; - /** The height of the most recently visible IME. */ - private float mImeHeight = 0f; - /** * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the * IME is not visible or the user moved the stack since the IME became visible. @@ -173,7 +170,7 @@ public class StackAnimationController extends */ private boolean mSpringToTouchOnNextMotionEvent = false; - /** Horizontal offset of bubbles in the stack. */ + /** Offset of bubbles in the stack (i.e. how much they overlap). */ private float mStackOffset; /** Offset between stack y and animation y for bubble swap. */ private float mSwapAnimationOffset; @@ -305,10 +302,7 @@ public class StackAnimationController extends if (mLayout == null || !isStackPositionSet()) { return true; // Default to left, which is where it starts by default. } - - float stackCenter = mStackPosition.x + mBubbleSize / 2; - float screenCenter = mLayout.getWidth() / 2; - return stackCenter < screenCenter; + return mPositioner.isStackOnLeft(mStackPosition); } /** @@ -524,16 +518,6 @@ public class StackAnimationController extends removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); } - /** Save the current IME height so that we know where the stack bounds should be. */ - public void setImeHeight(int imeHeight) { - mImeHeight = imeHeight; - } - - /** Returns the current IME height that the stack is offset by. */ - public float getImeHeight() { - return mImeHeight; - } - /** * Animates the stack either away from the newly visible IME, or back to its original position * due to the IME going away. @@ -592,11 +576,14 @@ public class StackAnimationController extends */ public RectF getAllowableStackPositionRegion() { final RectF allowableRegion = new RectF(mPositioner.getAvailableRect()); + final int imeHeight = mPositioner.getImeHeight(); + final float bottomPadding = getBubbleCount() > 1 + ? mBubblePaddingTop + mStackOffset + : mBubblePaddingTop; allowableRegion.left -= mBubbleOffscreen; allowableRegion.top += mBubblePaddingTop; allowableRegion.right += mBubbleOffscreen - mBubbleSize; - allowableRegion.bottom -= mBubblePaddingTop + mBubbleSize - + (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f); + allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; return allowableRegion; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java index 3a7b534f3c17..ffda1f92ec90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.common; import android.os.RemoteException; +import android.util.Slog; import android.view.IDisplayWindowRotationCallback; import android.view.IDisplayWindowRotationController; import android.view.IWindowManager; @@ -27,6 +28,7 @@ import androidx.annotation.BinderThread; import com.android.wm.shell.common.annotations.ShellMainThread; import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; /** * This module deals with display rotations coming from WM. When WM starts a rotation: after it has @@ -35,14 +37,14 @@ import java.util.ArrayList; * rotation. */ public class DisplayChangeController { + private static final String TAG = DisplayChangeController.class.getSimpleName(); private final ShellExecutor mMainExecutor; private final IWindowManager mWmService; private final IDisplayWindowRotationController mControllerImpl; - private final ArrayList<OnDisplayChangingListener> mRotationListener = - new ArrayList<>(); - private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>(); + private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener = + new CopyOnWriteArrayList<>(); public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; @@ -59,34 +61,26 @@ public class DisplayChangeController { * Adds a display rotation controller. */ public void addRotationListener(OnDisplayChangingListener listener) { - synchronized (mRotationListener) { - mRotationListener.add(listener); - } + mRotationListener.add(listener); } /** * Removes a display rotation controller. */ public void removeRotationListener(OnDisplayChangingListener listener) { - synchronized (mRotationListener) { - mRotationListener.remove(listener); - } + mRotationListener.remove(listener); } private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, IDisplayWindowRotationCallback callback) { WindowContainerTransaction t = new WindowContainerTransaction(); - synchronized (mRotationListener) { - mTmpListeners.clear(); - // Make a local copy in case the handlers add/remove themselves. - mTmpListeners.addAll(mRotationListener); - } - for (OnDisplayChangingListener c : mTmpListeners) { + for (OnDisplayChangingListener c : mRotationListener) { c.onRotateDisplay(displayId, fromRotation, toRotation, t); } try { callback.continueRotateDisplay(toRotation, t); } catch (RemoteException e) { + Slog.e(TAG, "Failed to continue rotation", e); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index ba9ba5e5883a..a1fb658ccb9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -26,6 +26,7 @@ import android.util.SparseArray; import android.view.Display; import android.view.IDisplayWindowListener; import android.view.IWindowManager; +import android.view.InsetsState; import androidx.annotation.BinderThread; @@ -52,14 +53,6 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); - /** - * Gets a display by id from DisplayManager. - */ - public Display getDisplay(int displayId) { - final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - return displayManager.getDisplay(displayId); - } - public DisplayController(Context context, IWindowManager wmService, ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; @@ -67,14 +60,31 @@ public class DisplayController { mWmService = wmService; mChangeController = new DisplayChangeController(mWmService, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); + } + + /** + * Initializes the window listener. + */ + public void initialize() { try { - mWmService.registerDisplayWindowListener(mDisplayContainerListener); + int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener); + for (int i = 0; i < displayIds.length; i++) { + onDisplayAdded(displayIds[i]); + } } catch (RemoteException e) { - throw new RuntimeException("Unable to register hierarchy listener"); + throw new RuntimeException("Unable to register display controller"); } } /** + * Gets a display by id from DisplayManager. + */ + public Display getDisplay(int displayId) { + final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + return displayManager.getDisplay(displayId); + } + + /** * Gets the DisplayLayout associated with a display. */ public @Nullable DisplayLayout getDisplayLayout(int displayId) { @@ -91,6 +101,16 @@ public class DisplayController { } /** + * Updates the insets for a given display. + */ + public void updateDisplayInsets(int displayId, InsetsState state) { + final DisplayRecord r = mDisplays.get(displayId); + if (r != null) { + r.setInsets(state); + } + } + + /** * Add a display window-container listener. It will get notified whenever a display's * configuration changes or when displays are added/removed from the WM hierarchy. */ @@ -134,17 +154,18 @@ public class DisplayController { if (mDisplays.get(displayId) != null) { return; } - Display display = getDisplay(displayId); + final Display display = getDisplay(displayId); if (display == null) { // It's likely that the display is private to some app and thus not // accessible by system-ui. return; } - DisplayRecord record = new DisplayRecord(); - record.mDisplayId = displayId; - record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext + + final Context context = (displayId == Display.DEFAULT_DISPLAY) + ? mContext : mContext.createDisplayContext(display); - record.mDisplayLayout = new DisplayLayout(record.mContext, display); + final DisplayRecord record = new DisplayRecord(displayId); + record.setDisplayLayout(context, new DisplayLayout(context, display)); mDisplays.put(displayId, record); for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { mDisplayChangedListeners.get(i).onDisplayAdded(displayId); @@ -154,24 +175,23 @@ public class DisplayController { private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { synchronized (mDisplays) { - DisplayRecord dr = mDisplays.get(displayId); + final DisplayRecord dr = mDisplays.get(displayId); if (dr == null) { Slog.w(TAG, "Skipping Display Configuration change on non-added" + " display."); return; } - Display display = getDisplay(displayId); + final Display display = getDisplay(displayId); if (display == null) { Slog.w(TAG, "Skipping Display Configuration change on invalid" + " display. It may have been removed."); return; } - Context perDisplayContext = mContext; - if (displayId != Display.DEFAULT_DISPLAY) { - perDisplayContext = mContext.createDisplayContext(display); - } - dr.mContext = perDisplayContext.createConfigurationContext(newConfig); - dr.mDisplayLayout = new DisplayLayout(dr.mContext, display); + final Context perDisplayContext = (displayId == Display.DEFAULT_DISPLAY) + ? mContext + : mContext.createDisplayContext(display); + final Context context = perDisplayContext.createConfigurationContext(newConfig); + dr.setDisplayLayout(context, new DisplayLayout(context, display)); for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { mDisplayChangedListeners.get(i).onDisplayConfigurationChanged( displayId, newConfig); @@ -219,9 +239,25 @@ public class DisplayController { } private static class DisplayRecord { - int mDisplayId; - Context mContext; - DisplayLayout mDisplayLayout; + private int mDisplayId; + private Context mContext; + private DisplayLayout mDisplayLayout; + private InsetsState mInsetsState = new InsetsState(); + + private DisplayRecord(int displayId) { + mDisplayId = displayId; + } + + private void setDisplayLayout(Context context, DisplayLayout displayLayout) { + mContext = context; + mDisplayLayout = displayLayout; + mDisplayLayout.setInsets(mContext.getResources(), mInsetsState); + } + + private void setInsets(InsetsState state) { + mInsetsState = state; + mDisplayLayout.setInsets(mContext.getResources(), state); + } } @BinderThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index a7996f056785..a7052bc49699 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -33,6 +33,7 @@ import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowInsets; @@ -68,14 +69,17 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged protected final Executor mMainExecutor; private final TransactionPool mTransactionPool; private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>(); private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>(); public DisplayImeController(IWindowManager wmService, DisplayController displayController, + DisplayInsetsController displayInsetsController, Executor mainExecutor, TransactionPool transactionPool) { mWmService = wmService; mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; mTransactionPool = transactionPool; } @@ -109,11 +113,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onDisplayRemoved(int displayId) { - try { - mWmService.setDisplayWindowInsetsController(displayId, null); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to remove insets controller on display " + displayId); + PerDisplay pd = mImePerDisplay.get(displayId); + if (pd == null) { + return; } + pd.unregister(); mImePerDisplay.remove(displayId); } @@ -195,11 +199,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */ - public class PerDisplay { + public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { final int mDisplayId; final InsetsState mInsetsState = new InsetsState(); - protected final DisplayWindowInsetsControllerImpl mInsetsControllerImpl = - new DisplayWindowInsetsControllerImpl(); + final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); InsetsSourceControl mImeSourceControl = null; int mAnimationDirection = DIRECTION_NONE; ValueAnimator mAnimation = null; @@ -214,14 +217,15 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } public void register() { - try { - mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId); - } + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); } - protected void insetsChanged(InsetsState insetsState) { + public void unregister() { + mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { if (mInsetsState.equals(insetsState)) { return; } @@ -239,8 +243,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + @Override @VisibleForTesting - protected void insetsControlChanged(InsetsState insetsState, + public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) { insetsChanged(insetsState); InsetsSourceControl imeSourceControl = null; @@ -279,9 +284,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (!mImeShowing) { removeImeSurface(); } - } - if (mImeSourceControl != null) { - mImeSourceControl.release(SurfaceControl::release); + if (mImeSourceControl != null) { + mImeSourceControl.release(SurfaceControl::release); + } } mImeSourceControl = imeSourceControl; } @@ -301,7 +306,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - protected void showInsets(int types, boolean fromIme) { + @Override + public void showInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { return; } @@ -309,8 +315,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged startAnimation(true /* show */, false /* forceRestart */); } - - protected void hideInsets(int types, boolean fromIme) { + @Override + public void hideInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { return; } @@ -318,6 +324,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged startAnimation(false /* show */, false /* forceRestart */); } + @Override public void topFocusedWindowChanged(String packageName) { // Do nothing } @@ -327,8 +334,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged */ private void setVisibleDirectly(boolean visible) { mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); + mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible); try { - mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState); + mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId, + mRequestedVisibilities); } catch (RemoteException e) { } } @@ -489,47 +498,6 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged dispatchVisibilityChanged(mDisplayId, isShowing); } } - - @VisibleForTesting - @BinderThread - public class DisplayWindowInsetsControllerImpl - extends IDisplayWindowInsetsController.Stub { - @Override - public void topFocusedWindowChanged(String packageName) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.topFocusedWindowChanged(packageName); - }); - } - - @Override - public void insetsChanged(InsetsState insetsState) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.insetsChanged(insetsState); - }); - } - - @Override - public void insetsControlChanged(InsetsState insetsState, - InsetsSourceControl[] activeControls) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.insetsControlChanged(insetsState, activeControls); - }); - } - - @Override - public void showInsets(int types, boolean fromIme) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.showInsets(types, fromIme); - }); - } - - @Override - public void hideInsets(int types, boolean fromIme) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.hideInsets(types, fromIme); - }); - } - } } void removeImeSurface() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java new file mode 100644 index 000000000000..565f1481233c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.os.RemoteException; +import android.util.Slog; +import android.util.SparseArray; +import android.view.IDisplayWindowInsetsController; +import android.view.IWindowManager; +import android.view.InsetsSourceControl; +import android.view.InsetsState; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.annotations.ShellMainThread; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Manages insets from the core. + */ +public class DisplayInsetsController implements DisplayController.OnDisplaysChangedListener { + private static final String TAG = "DisplayInsetsController"; + + private final IWindowManager mWmService; + private final ShellExecutor mMainExecutor; + private final DisplayController mDisplayController; + private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>(); + private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners = + new SparseArray<>(); + + public DisplayInsetsController(IWindowManager wmService, DisplayController displayController, + ShellExecutor mainExecutor) { + mWmService = wmService; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + } + + /** + * Starts listening for insets for each display. + **/ + public void initialize() { + mDisplayController.addDisplayWindowListener(this); + } + + /** + * Adds a callback to listen for insets changes for a particular display. Note that the + * listener will not be updated with the existing state of the insets on that display. + */ + public void addInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId); + if (listeners == null) { + listeners = new CopyOnWriteArrayList<>(); + mListeners.put(displayId, listeners); + } + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + /** + * Removes a callback listening for insets changes from a particular display. + */ + public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId); + if (listeners == null) { + return; + } + listeners.remove(listener); + } + + @Override + public void onDisplayAdded(int displayId) { + PerDisplay pd = new PerDisplay(displayId); + pd.register(); + mInsetsPerDisplay.put(displayId, pd); + } + + @Override + public void onDisplayRemoved(int displayId) { + PerDisplay pd = mInsetsPerDisplay.get(displayId); + if (pd == null) { + return; + } + pd.unregister(); + mInsetsPerDisplay.remove(displayId); + } + + /** + * An implementation of {@link IDisplayWindowInsetsController} for a given display id. + **/ + public class PerDisplay { + private final int mDisplayId; + private final DisplayWindowInsetsControllerImpl mInsetsControllerImpl = + new DisplayWindowInsetsControllerImpl(); + + public PerDisplay(int displayId) { + mDisplayId = displayId; + } + + public void register() { + try { + mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId); + } + } + + public void unregister() { + try { + mWmService.setDisplayWindowInsetsController(mDisplayId, null); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to remove insets controller on display " + mDisplayId); + } + } + + private void insetsChanged(InsetsState insetsState) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + mDisplayController.updateDisplayInsets(mDisplayId, insetsState); + for (OnInsetsChangedListener listener : listeners) { + listener.insetsChanged(insetsState); + } + } + + private void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.insetsControlChanged(insetsState, activeControls); + } + } + + private void showInsets(int types, boolean fromIme) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.showInsets(types, fromIme); + } + } + + private void hideInsets(int types, boolean fromIme) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.hideInsets(types, fromIme); + } + } + + private void topFocusedWindowChanged(String packageName) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.topFocusedWindowChanged(packageName); + } + } + + @BinderThread + private class DisplayWindowInsetsControllerImpl + extends IDisplayWindowInsetsController.Stub { + @Override + public void topFocusedWindowChanged(String packageName) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.topFocusedWindowChanged(packageName); + }); + } + + @Override + public void insetsChanged(InsetsState insetsState) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.insetsChanged(insetsState); + }); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.insetsControlChanged(insetsState, activeControls); + }); + } + + @Override + public void showInsets(int types, boolean fromIme) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.showInsets(types, fromIme); + }); + } + + @Override + public void hideInsets(int types, boolean fromIme) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.hideInsets(types, fromIme); + }); + } + } + } + + /** + * Gets notified whenever the insets change. + * + * @see IDisplayWindowInsetsController + */ + @ShellMainThread + public interface OnInsetsChangedListener { + /** + * Called when top focused window changes to determine whether or not to take over insets + * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false. + * @param packageName: Passes the top package name + */ + default void topFocusedWindowChanged(String packageName) {} + + /** + * Called when the window insets configuration has changed. + */ + default void insetsChanged(InsetsState insetsState) {} + + /** + * Called when this window retrieved control over a specified set of insets sources. + */ + default void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) {} + + /** + * Called when a set of insets source window should be shown by policy. + * + * @param types internal insets types (WindowInsets.Type.InsetsType) to show + * @param fromIme true if this request originated from IME (InputMethodService). + */ + default void showInsets(int types, boolean fromIme) {} + + /** + * Called when a set of insets source window should be hidden by policy. + * + * @param types internal insets types (WindowInsets.Type.InsetsType) to hide + * @param fromIme true if this request originated from IME (InputMethodService). + */ + default void hideInsets(int types, boolean fromIme) {} + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index b7235a31af03..962aca122b4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -25,6 +25,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON import static android.util.RotationUtils.rotateBounds; import static android.util.RotationUtils.rotateInsets; import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; +import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -44,7 +45,10 @@ import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.Gravity; +import android.view.InsetsSource; +import android.view.InsetsState; import android.view.Surface; +import android.view.WindowInsets; import com.android.internal.R; @@ -82,6 +86,10 @@ public class DisplayLayout { private boolean mHasNavigationBar = false; private boolean mHasStatusBar = false; private int mNavBarFrameHeight = 0; + private boolean mAllowSeamlessRotationDespiteNavBarMoving = false; + private boolean mNavigationBarCanMove = false; + private boolean mReverseDefaultRotation = false; + private InsetsState mInsetsState = new InsetsState(); @Override public boolean equals(Object o) { @@ -98,14 +106,20 @@ public class DisplayLayout { && Objects.equals(mStableInsets, other.mStableInsets) && mHasNavigationBar == other.mHasNavigationBar && mHasStatusBar == other.mHasStatusBar - && mNavBarFrameHeight == other.mNavBarFrameHeight; + && mAllowSeamlessRotationDespiteNavBarMoving + == other.mAllowSeamlessRotationDespiteNavBarMoving + && mNavigationBarCanMove == other.mNavigationBarCanMove + && mReverseDefaultRotation == other.mReverseDefaultRotation + && mNavBarFrameHeight == other.mNavBarFrameHeight + && Objects.equals(mInsetsState, other.mInsetsState); } @Override public int hashCode() { return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, - mNavBarFrameHeight); + mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, + mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState); } /** @@ -150,9 +164,13 @@ public class DisplayLayout { mDensityDpi = dl.mDensityDpi; mHasNavigationBar = dl.mHasNavigationBar; mHasStatusBar = dl.mHasStatusBar; + mAllowSeamlessRotationDespiteNavBarMoving = dl.mAllowSeamlessRotationDespiteNavBarMoving; + mNavigationBarCanMove = dl.mNavigationBarCanMove; + mReverseDefaultRotation = dl.mReverseDefaultRotation; mNavBarFrameHeight = dl.mNavBarFrameHeight; mNonDecorInsets.set(dl.mNonDecorInsets); mStableInsets.set(dl.mStableInsets); + mInsetsState.set(dl.mInsetsState, true /* copySources */); } private void init(DisplayInfo info, Resources res, boolean hasNavigationBar, @@ -165,12 +183,24 @@ public class DisplayLayout { mDensityDpi = info.logicalDensityDpi; mHasNavigationBar = hasNavigationBar; mHasStatusBar = hasStatusBar; + mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean( + R.bool.config_allowSeamlessRotationDespiteNavBarMoving); + mNavigationBarCanMove = res.getBoolean(R.bool.config_navBarCanMove); + mReverseDefaultRotation = res.getBoolean(R.bool.config_reverseDefaultRotation); + recalcInsets(res); + } + + /** + * Updates the current insets. + */ + public void setInsets(Resources res, InsetsState state) { + mInsetsState = state; recalcInsets(res); } private void recalcInsets(Resources res) { - computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets, - mHasNavigationBar); + computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mInsetsState, mUiMode, + mNonDecorInsets, mHasNavigationBar); mStableInsets.set(mNonDecorInsets); if (mHasStatusBar) { convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar); @@ -244,11 +274,33 @@ public class DisplayLayout { return mWidth > mHeight; } - /** Get the navbar frame height (used by ime). */ + /** Get the navbar frame (or window) height (used by ime). */ public int navBarFrameHeight() { return mNavBarFrameHeight; } + /** @return whether we can seamlessly rotate even if nav-bar can change sides. */ + public boolean allowSeamlessRotationDespiteNavBarMoving() { + return mAllowSeamlessRotationDespiteNavBarMoving; + } + + /** @return whether the navigation bar will change sides during rotation. */ + public boolean navigationBarCanMove() { + return mNavigationBarCanMove; + } + + /** @return the rotation that would make the physical display "upside down". */ + public int getUpsideDownRotation() { + boolean displayHardwareIsLandscape = mWidth > mHeight; + if ((mRotation % 2) != 0) { + displayHardwareIsLandscape = !displayHardwareIsLandscape; + } + if (displayHardwareIsLandscape) { + return mReverseDefaultRotation ? Surface.ROTATION_270 : Surface.ROTATION_90; + } + return Surface.ROTATION_180; + } + /** Gets the orientation of this layout */ public int getOrientation() { return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; @@ -291,21 +343,29 @@ public class DisplayLayout { * @param outInsets the insets to return */ static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth, - int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, - boolean hasNavigationBar) { + int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode, + Rect outInsets, boolean hasNavigationBar) { outInsets.setEmpty(); // Only navigation bar if (hasNavigationBar) { + final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR); + final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible(); int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation); int navBarSize = getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode); if (position == NAV_BAR_BOTTOM) { - outInsets.bottom = navBarSize; + outInsets.bottom = hasExtraNav + ? Math.max(navBarSize, extraNavBar.getFrame().height()) + : navBarSize; } else if (position == NAV_BAR_RIGHT) { - outInsets.right = navBarSize; + outInsets.right = hasExtraNav + ? Math.max(navBarSize, extraNavBar.getFrame().width()) + : navBarSize; } else if (position == NAV_BAR_LEFT) { - outInsets.left = navBarSize; + outInsets.left = hasExtraNav + ? Math.max(navBarSize, extraNavBar.getFrame().width()) + : navBarSize; } } @@ -327,13 +387,13 @@ public class DisplayLayout { * @param outInsets the insets to return */ static void computeStableInsets(Resources res, int displayRotation, int displayWidth, - int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, - boolean hasNavigationBar, boolean hasStatusBar) { + int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode, + Rect outInsets, boolean hasNavigationBar, boolean hasStatusBar) { outInsets.setEmpty(); // Navigation bar and status bar. computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout, - uiMode, outInsets, hasNavigationBar); + insetsState, uiMode, outInsets, hasNavigationBar); convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight, hasStatusBar); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java index 33beab5ee3f1..4c0281dcc517 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java @@ -18,13 +18,15 @@ package com.android.wm.shell.common; import android.annotation.BinderThread; import android.annotation.NonNull; +import android.os.RemoteException; import android.util.Slog; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import android.window.WindowOrganizer; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.transition.LegacyTransitions; import java.util.ArrayList; @@ -66,6 +68,10 @@ public final class SyncTransactionQueue { * Queues a sync transaction to be sent serially to WM. */ public void queue(WindowContainerTransaction wct) { + if (wct.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty"); + return; + } SyncCallback cb = new SyncCallback(wct); synchronized (mQueue) { if (DEBUG) Slog.d(TAG, "Queueing up " + wct); @@ -77,11 +83,34 @@ public final class SyncTransactionQueue { } /** + * Queues a legacy transition to be sent serially to WM + */ + public void queue(LegacyTransitions.ILegacyTransition transition, + @WindowManager.TransitionType int type, WindowContainerTransaction wct) { + if (wct.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty"); + return; + } + SyncCallback cb = new SyncCallback(transition, type, wct); + synchronized (mQueue) { + if (DEBUG) Slog.d(TAG, "Queueing up legacy transition " + wct); + mQueue.add(cb); + if (mQueue.size() == 1) { + cb.send(); + } + } + } + + /** * Queues a sync transaction only if there are already sync transaction(s) queued or in flight. * Otherwise just returns without queueing. * @return {@code true} if queued, {@code false} if not. */ public boolean queueIfWaiting(WindowContainerTransaction wct) { + if (wct.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Skip queueIfWaiting due to transaction change is empty"); + return false; + } synchronized (mQueue) { if (mQueue.isEmpty()) { if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct); @@ -118,12 +147,12 @@ public final class SyncTransactionQueue { // Synchronized on mQueue private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) { if (DEBUG) Slog.d(TAG, " Running " + mRunnables.size() + " sync runnables"); - for (int i = 0, n = mRunnables.size(); i < n; ++i) { + final int n = mRunnables.size(); + for (int i = 0; i < n; ++i) { mRunnables.get(i).runWithTransaction(t); } - mRunnables.clear(); - t.apply(); - t.close(); + // More runnables may have been added, so only remove the ones that ran. + mRunnables.subList(0, n).clear(); } /** Task to run with transaction. */ @@ -135,20 +164,38 @@ public final class SyncTransactionQueue { private class SyncCallback extends WindowContainerTransactionCallback { int mId = -1; final WindowContainerTransaction mWCT; + final LegacyTransitions.LegacyTransition mLegacyTransition; SyncCallback(WindowContainerTransaction wct) { mWCT = wct; + mLegacyTransition = null; + } + + SyncCallback(LegacyTransitions.ILegacyTransition legacyTransition, + @WindowManager.TransitionType int type, WindowContainerTransaction wct) { + mWCT = wct; + mLegacyTransition = new LegacyTransitions.LegacyTransition(type, legacyTransition); } // Must be sychronized on mQueue void send() { + if (mInFlight == this) { + // This was probably queued up and sent during a sync runnable of the last callback. + // Don't queue it again. + return; + } if (mInFlight != null) { throw new IllegalStateException("Sync Transactions must be serialized. In Flight: " + mInFlight.mId + " - " + mInFlight.mWCT); } mInFlight = this; if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT); - mId = new WindowOrganizer().applySyncTransaction(mWCT, this); + if (mLegacyTransition != null) { + mId = new WindowOrganizer().startLegacyTransition(mLegacyTransition.getType(), + mLegacyTransition.getAdapter(), this, mWCT); + } else { + mId = new WindowOrganizer().applySyncTransaction(mWCT, this); + } if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId); mMainExecutor.executeDelayed(mOnReplyTimeout, REPLY_TIMEOUT); } @@ -169,6 +216,16 @@ public final class SyncTransactionQueue { if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId); mQueue.remove(this); onTransactionReceived(t); + if (mLegacyTransition != null) { + try { + mLegacyTransition.getSyncCallback().onTransactionReady(mId, t); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e); + } + } else { + t.apply(); + t.close(); + } if (!mQueue.isEmpty()) { mQueue.get(0).send(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java index 218bf47e24aa..c76937de6669 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java @@ -70,7 +70,8 @@ public class DividerHandleView extends View { private final Paint mPaint = new Paint(); private final int mWidth; private final int mHeight; - private final int mCircleDiameter; + private final int mTouchingWidth; + private final int mTouchingHeight; private int mCurrentWidth; private int mCurrentHeight; private AnimatorSet mAnimator; @@ -80,11 +81,12 @@ public class DividerHandleView extends View { super(context, attrs); mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null)); mPaint.setAntiAlias(true); - mWidth = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_width); - mHeight = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_height); + mWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_width); + mHeight = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_height); mCurrentWidth = mWidth; mCurrentHeight = mHeight; - mCircleDiameter = (mWidth + mHeight) / 3; + mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth; + mTouchingHeight = mHeight > mWidth ? mHeight / 2 : mHeight; } /** Sets touching state for this handle view. */ @@ -98,16 +100,16 @@ public class DividerHandleView extends View { } if (!animate) { if (touching) { - mCurrentWidth = mCircleDiameter; - mCurrentHeight = mCircleDiameter; + mCurrentWidth = mTouchingWidth; + mCurrentHeight = mTouchingHeight; } else { mCurrentWidth = mWidth; mCurrentHeight = mHeight; } invalidate(); } else { - animateToTarget(touching ? mCircleDiameter : mWidth, - touching ? mCircleDiameter : mHeight, touching); + animateToTarget(touching ? mTouchingWidth : mWidth, + touching ? mTouchingHeight : mHeight, touching); } mTouching = touching; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java new file mode 100644 index 000000000000..364bb651d55d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; +import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; +import static android.view.RoundedCorner.POSITION_TOP_LEFT; +import static android.view.RoundedCorner.POSITION_TOP_RIGHT; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.util.AttributeSet; +import android.view.RoundedCorner; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** + * Draws inverted rounded corners beside divider bar to keep splitting tasks cropped with proper + * rounded corners. + */ +public class DividerRoundedCorner extends View { + private final int mDividerWidth; + private final Paint mDividerBarBackground; + private final Point mStartPos = new Point(); + private InvertedRoundedCornerDrawInfo mTopLeftCorner; + private InvertedRoundedCornerDrawInfo mTopRightCorner; + private InvertedRoundedCornerDrawInfo mBottomLeftCorner; + private InvertedRoundedCornerDrawInfo mBottomRightCorner; + + public DividerRoundedCorner(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mDividerWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width); + mDividerBarBackground = new Paint(); + mDividerBarBackground.setColor( + getResources().getColor(R.color.split_divider_background, null)); + mDividerBarBackground.setFlags(Paint.ANTI_ALIAS_FLAG); + mDividerBarBackground.setStyle(Paint.Style.FILL); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mTopLeftCorner = new InvertedRoundedCornerDrawInfo(POSITION_TOP_LEFT); + mTopRightCorner = new InvertedRoundedCornerDrawInfo(POSITION_TOP_RIGHT); + mBottomLeftCorner = new InvertedRoundedCornerDrawInfo(POSITION_BOTTOM_LEFT); + mBottomRightCorner = new InvertedRoundedCornerDrawInfo(POSITION_BOTTOM_RIGHT); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.save(); + + mTopLeftCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mTopLeftCorner.mPath, mDividerBarBackground); + + canvas.translate(-mStartPos.x, -mStartPos.y); + mTopRightCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mTopRightCorner.mPath, mDividerBarBackground); + + canvas.translate(-mStartPos.x, -mStartPos.y); + mBottomLeftCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mBottomLeftCorner.mPath, mDividerBarBackground); + + canvas.translate(-mStartPos.x, -mStartPos.y); + mBottomRightCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mBottomRightCorner.mPath, mDividerBarBackground); + + canvas.restore(); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private boolean isLandscape() { + return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + } + + /** + * Holds draw information of the inverted rounded corner at a specific position. + * + * @see {@link com.android.launcher3.taskbar.TaskbarDragLayer} + */ + private class InvertedRoundedCornerDrawInfo { + @RoundedCorner.Position + private final int mCornerPosition; + + private final int mRadius; + + private final Path mPath = new Path(); + + InvertedRoundedCornerDrawInfo(@RoundedCorner.Position int cornerPosition) { + mCornerPosition = cornerPosition; + + final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(cornerPosition); + mRadius = roundedCorner == null ? 0 : roundedCorner.getRadius(); + + // Starts with a filled square, and then subtracting out a circle from the appropriate + // corner. + final Path square = new Path(); + square.addRect(0, 0, mRadius, mRadius, Path.Direction.CW); + final Path circle = new Path(); + circle.addCircle( + isLeftCorner() ? mRadius : 0 /* x */, + isTopCorner() ? mRadius : 0 /* y */, + mRadius, Path.Direction.CW); + mPath.op(square, circle, Path.Op.DIFFERENCE); + } + + private void calculateStartPos(Point outPos) { + if (isLandscape()) { + // Place left corner at the right side of the divider bar. + outPos.x = isLeftCorner() + ? getWidth() / 2 + mDividerWidth / 2 + : getWidth() / 2 - mDividerWidth / 2 - mRadius; + outPos.y = isTopCorner() ? 0 : getHeight() - mRadius; + } else { + outPos.x = isLeftCorner() ? 0 : getWidth() - mRadius; + // Place top corner at the bottom of the divider bar. + outPos.y = isTopCorner() + ? getHeight() / 2 + mDividerWidth / 2 + : getHeight() / 2 - mDividerWidth / 2 - mRadius; + } + } + + private boolean isLeftCorner() { + return mCornerPosition == POSITION_TOP_LEFT || mCornerPosition == POSITION_BOTTOM_LEFT; + } + + private boolean isTopCorner() { + return mCornerPosition == POSITION_TOP_LEFT || mCornerPosition == POSITION_TOP_RIGHT; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index cba019a11b28..6ea806bd3799 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -19,14 +19,21 @@ package com.android.wm.shell.common.split; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Property; import android.view.GestureDetector; +import android.view.InsetsController; +import android.view.InsetsSource; +import android.view.InsetsState; import android.view.MotionEvent; import android.view.SurfaceControlViewHost; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; @@ -44,6 +51,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { public static final long TOUCH_ANIMATION_DURATION = 150; public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; + /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */ + private float mExpandedTaskBarHeight; + private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private SplitLayout mSplitLayout; @@ -58,6 +68,31 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private GestureDetector mDoubleTapDetector; private boolean mInteractive; + /** + * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with + * insets. + */ + private final Rect mDividerBounds = new Rect(); + private final Rect mTempRect = new Rect(); + private FrameLayout mDividerBar; + + + static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY = + new Property<DividerView, Integer>(Integer.class, "height") { + @Override + public Integer get(DividerView object) { + return object.mDividerBar.getLayoutParams().height; + } + + @Override + public void set(DividerView object, Integer value) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) + object.mDividerBar.getLayoutParams(); + lp.height = value; + object.mDividerBar.setLayoutParams(lp); + } + }; + public DividerView(@NonNull Context context) { super(context); } @@ -79,16 +114,46 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { /** Sets up essential dependencies of the divider bar. */ public void setup( SplitLayout layout, - SurfaceControlViewHost viewHost) { + SurfaceControlViewHost viewHost, + InsetsState insetsState) { mSplitLayout = layout; mViewHost = viewHost; + mDividerBounds.set(layout.getDividerBounds()); + onInsetsChanged(insetsState, false /* animate */); + } + + void onInsetsChanged(InsetsState insetsState, boolean animate) { + mTempRect.set(mSplitLayout.getDividerBounds()); + final InsetsSource taskBarInsetsSource = + insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + // Only insets the divider bar with task bar when it's expanded so that the rounded corners + // will be drawn against task bar. + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect)); + } + + if (!mTempRect.equals(mDividerBounds)) { + if (animate) { + ObjectAnimator animator = ObjectAnimator.ofInt(this, + DIVIDER_HEIGHT_PROPERTY, mDividerBounds.height(), mTempRect.height()); + animator.setInterpolator(InsetsController.RESIZE_INTERPOLATOR); + animator.setDuration(InsetsController.ANIMATION_DURATION_RESIZE); + animator.start(); + } else { + DIVIDER_HEIGHT_PROPERTY.set(this, mTempRect.height()); + } + mDividerBounds.set(mTempRect); + } } @Override protected void onFinishInflate() { super.onFinishInflate(); + mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); mBackground = findViewById(R.id.docked_divider_background); + mExpandedTaskBarHeight = getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); @@ -106,10 +171,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return true; } + // Convert to use screen-based coordinates to prevent lost track of motion events while + // moving divider bar and calculating dragging velocity. + event.setLocation(event.getRawX(), event.getRawY()); final int action = event.getAction() & MotionEvent.ACTION_MASK; final boolean isLandscape = isLandscape(); - // Using raw xy to prevent lost track of motion events while moving divider bar. - final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY(); + final int touchPos = (int) (isLandscape ? event.getX() : event.getY()); switch (action) { case MotionEvent.ACTION_DOWN: mVelocityTracker = VelocityTracker.obtain(); @@ -153,16 +220,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private void setTouching() { setSlippery(false); mHandle.setTouching(true, true); - if (isLandscape()) { - mBackground.animate().scaleX(1.4f); - } else { - mBackground.animate().scaleY(1.4f); - } - mBackground.animate() - .setInterpolator(Interpolators.TOUCH_RESPONSE) - .setDuration(TOUCH_ANIMATION_DURATION) - .translationZ(mTouchElevation) - .start(); // Lift handle as well so it doesn't get behind the background, even though it doesn't // cast shadow. mHandle.animate() @@ -175,13 +232,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private void releaseTouching() { setSlippery(true); mHandle.setTouching(false, true); - mBackground.animate() - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) - .translationZ(0) - .scaleX(1f) - .scaleY(1f) - .start(); mHandle.animate() .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 5b158d2063ba..5b3ce2dbaeb9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -16,11 +16,19 @@ package com.android.wm.shell.common.split; +import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; +import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; +import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -30,7 +38,10 @@ import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.Rect; +import android.view.InsetsSourceControl; +import android.view.InsetsState; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; @@ -39,16 +50,19 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.internal.policy.DockedDividerUtils; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; /** * Records and handles layout of splits. Helps to calculate proper bounds when configuration or * divide position changes. */ -public final class SplitLayout { +public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { /** * Split position isn't specified normally meaning to use what ever it is currently set to. */ @@ -78,32 +92,44 @@ public final class SplitLayout { private final int mDividerInsets; private final int mDividerSize; + private final Rect mTempRect = new Rect(); private final Rect mRootBounds = new Rect(); private final Rect mDividerBounds = new Rect(); private final Rect mBounds1 = new Rect(); private final Rect mBounds2 = new Rect(); + private final Rect mWinBounds1 = new Rect(); + private final Rect mWinBounds2 = new Rect(); private final SplitLayoutHandler mSplitLayoutHandler; private final SplitWindowManager mSplitWindowManager; private final DisplayImeController mDisplayImeController; private final ImePositionProcessor mImePositionProcessor; + private final DismissingParallaxPolicy mDismissingParallaxPolicy; private final ShellTaskOrganizer mTaskOrganizer; + private final InsetsState mInsetsState = new InsetsState(); private Context mContext; private DividerSnapAlgorithm mDividerSnapAlgorithm; + private WindowContainerToken mWinToken1; + private WindowContainerToken mWinToken2; private int mDividePosition; private boolean mInitialized = false; + private int mOrientation; + private int mRotation; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer) { mContext = context.createConfigurationContext(configuration); + mOrientation = configuration.orientation; + mRotation = configuration.windowConfiguration.getRotation(); mSplitLayoutHandler = splitLayoutHandler; mDisplayImeController = displayImeController; - mSplitWindowManager = new SplitWindowManager( - windowName, mContext, configuration, parentContainerCallbacks); + mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, + parentContainerCallbacks); mTaskOrganizer = taskOrganizer; mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); + mDismissingParallaxPolicy = new DismissingParallaxPolicy(); final Resources resources = context.getResources(); mDividerWindowWidth = resources.getDimensionPixelSize( @@ -142,35 +168,71 @@ public final class SplitLayout { return mDividePosition; } + /** + * Returns the divider position as a fraction from 0 to 1. + */ + public float getDividerPositionAsFraction() { + return Math.min(1f, Math.max(0f, isLandscape() + ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right + : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); + } + /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ public boolean updateConfiguration(Configuration configuration) { + boolean affectsLayout = false; + + // Update the split bounds when necessary. Besides root bounds changed, split bounds need to + // be updated when the rotation changed to cover the case that users rotated the screen 180 + // degrees. + // Make sure to render the divider bar with proper resources that matching the screen + // orientation. + final int rotation = configuration.windowConfiguration.getRotation(); final Rect rootBounds = configuration.windowConfiguration.getBounds(); - if (mRootBounds.equals(rootBounds)) { + final int orientation = configuration.orientation; + + if (mOrientation == orientation + && rotation == mRotation + && mRootBounds.equals(rootBounds)) { return false; } mContext = mContext.createConfigurationContext(configuration); mSplitWindowManager.setConfiguration(configuration); + mOrientation = orientation; + mTempRect.set(mRootBounds); mRootBounds.set(rootBounds); + mRotation = rotation; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); - resetDividerPosition(); + initDividerPosition(mTempRect); - // Don't inflate divider bar if it is not initialized. - if (!mInitialized) { - return false; + if (mInitialized) { + release(); + init(); } - release(); - init(); return true; } + private void initDividerPosition(Rect oldBounds) { + final float snapRatio = (float) mDividePosition + / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height()); + // Estimate position by previous ratio. + final float length = + (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height()); + final int estimatePosition = (int) (length * snapRatio); + // Init divider position by estimated position using current bounds snap algorithm. + mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( + estimatePosition).position; + updateBounds(mDividePosition); + } + /** Updates recording bounds of divider window and both of the splits. */ private void updateBounds(int position) { mDividerBounds.set(mRootBounds); mBounds1.set(mRootBounds); mBounds2.set(mRootBounds); - if (isLandscape(mRootBounds)) { + final boolean isLandscape = isLandscape(mRootBounds); + if (isLandscape) { position += mRootBounds.left; mDividerBounds.left = position - mDividerInsets; mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; @@ -183,13 +245,16 @@ public final class SplitLayout { mBounds1.bottom = position; mBounds2.top = mBounds1.bottom + mDividerSize; } + DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */); + DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */); + mDismissingParallaxPolicy.applyDividerPosition(position, isLandscape); } /** Inflates {@link DividerView} on the root surface. */ public void init() { if (mInitialized) return; mInitialized = true; - mSplitWindowManager.init(this); + mSplitWindowManager.init(this, mInsetsState); mDisplayImeController.addPositionProcessor(mImePositionProcessor); } @@ -202,6 +267,23 @@ public final class SplitLayout { mImePositionProcessor.reset(); } + @Override + public void insetsChanged(InsetsState insetsState) { + mInsetsState.set(insetsState); + if (!mInitialized) { + return; + } + mSplitWindowManager.onInsetsChanged(insetsState); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + if (!mInsetsState.equals(insetsState)) { + insetsChanged(insetsState); + } + } + /** * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. @@ -209,20 +291,25 @@ public final class SplitLayout { void updateDivideBounds(int position) { updateBounds(position); mSplitWindowManager.setResizingSplits(true); - mSplitLayoutHandler.onBoundsChanging(this); + mSplitLayoutHandler.onLayoutSizeChanging(this); } void setDividePosition(int position) { mDividePosition = position; updateBounds(mDividePosition); - mSplitLayoutHandler.onBoundsChanged(this); + mSplitLayoutHandler.onLayoutSizeChanged(this); mSplitWindowManager.setResizingSplits(false); } /** Resets divider position. */ public void resetDividerPosition() { mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; + mSplitWindowManager.setResizingSplits(false); updateBounds(mDividePosition); + mWinToken1 = null; + mWinToken2 = null; + mWinBounds1.setEmpty(); + mWinBounds2.setEmpty(); } /** @@ -232,15 +319,15 @@ public final class SplitLayout { public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { switch (snapTarget.flag) { case FLAG_DISMISS_START: - mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */); - mSplitWindowManager.setResizingSplits(false); + flingDividePosition(currentPosition, snapTarget.position, + () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */)); break; case FLAG_DISMISS_END: - mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */); - mSplitWindowManager.setResizingSplits(false); + flingDividePosition(currentPosition, snapTarget.position, + () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */)); break; default: - flingDividePosition(currentPosition, snapTarget.position); + flingDividePosition(currentPosition, snapTarget.position, null); break; } } @@ -270,8 +357,13 @@ public final class SplitLayout { isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } - private void flingDividePosition(int from, int to) { - if (from == to) return; + @VisibleForTesting + void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) { + if (from == to) { + // No animation run, it should stop resizing here. + mSplitWindowManager.setResizingSplits(false); + return; + } ValueAnimator animator = ValueAnimator .ofInt(from, to) .setDuration(250); @@ -282,6 +374,9 @@ public final class SplitLayout { @Override public void onAnimationEnd(Animator animation) { setDividePosition(to); + if (flingFinishedCallback != null) { + flingFinishedCallback.run(); + } } @Override @@ -296,42 +391,99 @@ public final class SplitLayout { return context.getSystemService(WindowManager.class) .getMaximumWindowMetrics() .getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars() - | WindowInsets.Type.statusBars() - | WindowInsets.Type.displayCutout()).toRect(); + .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()) + .toRect(); } private static boolean isLandscape(Rect bounds) { return bounds.width() > bounds.height(); } + /** + * Return if this layout is landscape. + */ + public boolean isLandscape() { + return isLandscape(mRootBounds); + } + /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { - final Rect dividerBounds = mImePositionProcessor.adjustForIme(mDividerBounds); - final Rect bounds1 = mImePositionProcessor.adjustForIme(mBounds1); - final Rect bounds2 = mImePositionProcessor.adjustForIme(mBounds2); final SurfaceControl dividerLeash = getDividerLeash(); if (dividerLeash != null) { - t.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) - // Resets layer of divider bar to make sure it is always on top. - .setLayer(dividerLeash, Integer.MAX_VALUE); + t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top); + // Resets layer of divider bar to make sure it is always on top. + t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER); + } + t.setPosition(leash1, mBounds1.left, mBounds1.top) + .setWindowCrop(leash1, mBounds1.width(), mBounds1.height()); + t.setPosition(leash2, mBounds2.left, mBounds2.top) + .setWindowCrop(leash2, mBounds2.width(), mBounds2.height()); + + if (mImePositionProcessor.adjustSurfaceLayoutForIme( + t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { + return; } - t.setPosition(leash1, bounds1.left, bounds1.top) - .setWindowCrop(leash1, bounds1.width(), bounds1.height()); - - t.setPosition(leash2, bounds2.left, bounds2.top) - .setWindowCrop(leash2, bounds2.width(), bounds2.height()); - - mImePositionProcessor.applySurfaceDimValues(t, dimLayer1, dimLayer2); + mDismissingParallaxPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2); } /** Apply recorded task layout to the {@link WindowContainerTransaction}. */ public void applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { - wct.setBounds(task1.token, mImePositionProcessor.adjustForIme(mBounds1)) - .setBounds(task2.token, mImePositionProcessor.adjustForIme(mBounds2)); + if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) { + return; + } + + if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { + wct.setBounds(task1.token, mBounds1); + mWinBounds1.set(mBounds1); + mWinToken1 = task1.token; + } + if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { + wct.setBounds(task2.token, mBounds2); + mWinBounds2.set(mBounds2); + mWinToken2 = task2.token; + } + } + + /** + * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And + * restore shifted configuration bounds if it's no longer shifted. + */ + public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, + ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { + if (offsetX == 0 && offsetY == 0) { + wct.setBounds(taskInfo1.token, mBounds1); + wct.setAppBounds(taskInfo1.token, null); + wct.setScreenSizeDp(taskInfo1.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + + wct.setBounds(taskInfo2.token, mBounds2); + wct.setAppBounds(taskInfo2.token, null); + wct.setScreenSizeDp(taskInfo2.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + } else { + mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setBounds(taskInfo1.token, mTempRect); + mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setAppBounds(taskInfo1.token, mTempRect); + wct.setScreenSizeDp(taskInfo1.token, + taskInfo1.configuration.screenWidthDp, + taskInfo1.configuration.screenHeightDp); + + mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setBounds(taskInfo2.token, mTempRect); + mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setAppBounds(taskInfo2.token, mTempRect); + wct.setScreenSizeDp(taskInfo2.token, + taskInfo2.configuration.screenWidthDp, + taskInfo2.configuration.screenHeightDp); + } } /** Handles layout change event. */ @@ -340,11 +492,43 @@ public final class SplitLayout { /** Calls when dismissing split. */ void onSnappedToDismiss(boolean snappedToEnd); - /** Calls when the bounds is changing due to animation or dragging divider bar. */ - void onBoundsChanging(SplitLayout layout); + /** + * Calls when resizing the split bounds. + * + * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, + * SurfaceControl, SurfaceControl) + */ + void onLayoutSizeChanging(SplitLayout layout); + + /** + * Calls when finish resizing the split bounds. + * + * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo, + * ActivityManager.RunningTaskInfo) + * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, + * SurfaceControl, SurfaceControl) + */ + void onLayoutSizeChanged(SplitLayout layout); + + /** + * Calls when re-positioning the split bounds. Like moving split bounds while showing IME + * panel. + * + * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, + * SurfaceControl, SurfaceControl) + */ + void onLayoutPositionChanging(SplitLayout layout); - /** Calls when the target bounds changed. */ - void onBoundsChanged(SplitLayout layout); + /** + * Notifies the target offset for shifting layout. So layout handler can shift configuration + * bounds correspondingly to make sure client apps won't get configuration changed or + * relaunched. If the layout is no longer shifted, layout handler should restore shifted + * configuration bounds. + * + * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int, + * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo) + */ + void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout); /** Calls when user double tapped on the divider bar. */ default void onDoubleTappedDivider() { @@ -355,6 +539,106 @@ public final class SplitLayout { int getSplitItemPosition(WindowContainerToken token); } + /** + * Calculates and applies proper dismissing parallax offset and dimming value to hint users + * dismissing gesture. + */ + private class DismissingParallaxPolicy { + // The current dismissing side. + int mDismissingSide = DOCKED_INVALID; + + // The parallax offset to hint the dismissing side and progress. + final Point mDismissingParallaxOffset = new Point(); + + // The dimming value to hint the dismissing side and progress. + float mDismissingDimValue = 0.0f; + + /** + * Applies a parallax to the task to hint dismissing progress. + * + * @param position the split position to apply dismissing parallax effect + * @param isLandscape indicates whether it's splitting horizontally or vertically + */ + void applyDividerPosition(int position, boolean isLandscape) { + mDismissingSide = DOCKED_INVALID; + mDismissingParallaxOffset.set(0, 0); + mDismissingDimValue = 0; + + int totalDismissingDistance = 0; + if (position <= mDividerSnapAlgorithm.getFirstSplitTarget().position) { + mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position + - mDividerSnapAlgorithm.getFirstSplitTarget().position; + } else if (position >= mDividerSnapAlgorithm.getLastSplitTarget().position) { + mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position + - mDividerSnapAlgorithm.getDismissEndTarget().position; + } + + if (mDismissingSide != DOCKED_INVALID) { + float fraction = Math.max(0, + Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f)); + mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); + fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); + if (isLandscape) { + mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance); + } else { + mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance); + } + } + } + + /** + * @return for a specified {@code fraction}, this returns an adjusted value that simulates a + * slowing down parallax effect + */ + private float calculateParallaxDismissingFraction(float fraction, int dockSide) { + float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; + + // Less parallax at the top, just because. + if (dockSide == WindowManager.DOCKED_TOP) { + result /= 2f; + } + return result; + } + + /** Applies parallax offset and dimming value to the root surface at the dismissing side. */ + boolean adjustDismissingSurface(SurfaceControl.Transaction t, + SurfaceControl leash1, SurfaceControl leash2, + SurfaceControl dimLayer1, SurfaceControl dimLayer2) { + SurfaceControl targetLeash, targetDimLayer; + switch (mDismissingSide) { + case DOCKED_TOP: + case DOCKED_LEFT: + targetLeash = leash1; + targetDimLayer = dimLayer1; + mTempRect.set(mBounds1); + break; + case DOCKED_BOTTOM: + case DOCKED_RIGHT: + targetLeash = leash2; + targetDimLayer = dimLayer2; + mTempRect.set(mBounds2); + break; + case DOCKED_INVALID: + default: + t.setAlpha(dimLayer1, 0).hide(dimLayer1); + t.setAlpha(dimLayer2, 0).hide(dimLayer2); + return false; + } + + t.setPosition(targetLeash, + mTempRect.left + mDismissingParallaxOffset.x, + mTempRect.top + mDismissingParallaxOffset.y); + // Transform the screen-based split bounds to surface-based crop bounds. + mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y); + t.setWindowCrop(targetLeash, mTempRect); + t.setAlpha(targetDimLayer, mDismissingDimValue) + .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); + return true; + } + } + /** Records IME top offset changes and updates SplitLayout correspondingly. */ private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor { /** @@ -409,6 +693,18 @@ public final class SplitLayout { && !isFloating && !isLandscape(mRootBounds) && showing; mTargetYOffset = needOffset ? getTargetYOffset() : 0; + if (mTargetYOffset != mLastYOffset) { + // Freeze the configuration size with offset to prevent app get a configuration + // changed or relaunch. This is required to make sure client apps will calculate + // insets properly after layout shifted. + if (mTargetYOffset == 0) { + mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); + } else { + mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset, + SplitLayout.this); + } + } + // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough // because DividerView won't receive onImeVisibilityChanged callback after it being @@ -423,7 +719,7 @@ public final class SplitLayout { public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { if (displayId != mDisplayId) return; onProgress(getProgress(imeTop)); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @Override @@ -431,7 +727,7 @@ public final class SplitLayout { SurfaceControl.Transaction t) { if (displayId != mDisplayId || cancel) return; onProgress(1.0f); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @Override @@ -441,7 +737,7 @@ public final class SplitLayout { if (!controlling && mImeShown) { reset(); mSplitWindowManager.setInteractive(true); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } } @@ -473,24 +769,66 @@ public final class SplitLayout { return start + (end - start) * progress; } - private void reset() { + void reset() { mImeShown = false; mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f; } - /* Adjust bounds with IME offset. */ - private Rect adjustForIme(Rect bounds) { - final Rect temp = new Rect(bounds); - if (mYOffsetForIme != 0) temp.offset(0, mYOffsetForIme); - return temp; + /** + * Applies adjusted task layout for showing IME. + * + * @return {@code false} if there's no need to adjust, otherwise {@code true} + */ + boolean applyTaskLayoutForIme(WindowContainerTransaction wct, + WindowContainerToken token1, WindowContainerToken token2) { + if (mYOffsetForIme == 0) return false; + + mTempRect.set(mBounds1); + mTempRect.offset(0, mYOffsetForIme); + wct.setBounds(token1, mTempRect); + + mTempRect.set(mBounds2); + mTempRect.offset(0, mYOffsetForIme); + wct.setBounds(token2, mTempRect); + + return true; } - private void applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1, - SurfaceControl dimLayer2) { - t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); - t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); + /** + * Adjusts surface layout while showing IME. + * + * @return {@code false} if there's no need to adjust, otherwise {@code true} + */ + boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, + SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, + SurfaceControl dimLayer1, SurfaceControl dimLayer2) { + final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f; + boolean adjusted = false; + if (mYOffsetForIme != 0) { + if (dividerLeash != null) { + mTempRect.set(mDividerBounds); + mTempRect.offset(0, mYOffsetForIme); + t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); + } + + mTempRect.set(mBounds1); + mTempRect.offset(0, mYOffsetForIme); + t.setPosition(leash1, mTempRect.left, mTempRect.top); + + mTempRect.set(mBounds2); + mTempRect.offset(0, mYOffsetForIme); + t.setPosition(leash2, mTempRect.left, mTempRect.top); + adjusted = true; + } + + if (showDim) { + t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); + t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); + adjusted = true; + } + return adjusted; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 0cea0efc0057..47dceb392183 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -36,6 +36,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; import android.view.IWindow; +import android.view.InsetsState; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; @@ -63,6 +64,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { public interface ParentContainerCallbacks { void attachToParentSurface(SurfaceControl.Builder b); + void onLeashReady(SurfaceControl leash); } public SplitWindowManager(String windowName, Context context, Configuration config, @@ -95,15 +97,16 @@ public final class SplitWindowManager extends WindowlessWindowManager { final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) .setContainerLayer() .setName(TAG) - .setHidden(false) + .setHidden(true) .setCallsite("SplitWindowManager#attachToParentSurface"); mParentContainerCallbacks.attachToParentSurface(builder); mLeash = builder.build(); + mParentContainerCallbacks.onLeashReady(mLeash); b.setParent(mLeash); } /** Inflates {@link DividerView} on to the root surface. */ - void init(SplitLayout splitLayout) { + void init(SplitLayout splitLayout, InsetsState insetsState) { if (mDividerView != null || mViewHost != null) { throw new UnsupportedOperationException( "Try to inflate divider view again without release first"); @@ -123,7 +126,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { lp.setTitle(mWindowName); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; mViewHost.setView(mDividerView, lp); - mDividerView.setup(splitLayout, mViewHost); + mDividerView.setup(splitLayout, mViewHost, insetsState); } /** @@ -169,4 +172,10 @@ public final class SplitWindowManager extends WindowlessWindowManager { SurfaceControl getSurfaceControl() { return mLeash; } + + void onInsetsChanged(InsetsState insetsState) { + if (mDividerView != null) { + mDividerView.onInsetsChanged(insetsState, true /* animate */); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java new file mode 100644 index 000000000000..defbd5af01d9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.displayareahelper; + +import android.view.SurfaceControl; + +import java.util.function.Consumer; + +/** + * Interface that allows to perform various display area related actions + */ +public interface DisplayAreaHelper { + + /** + * Updates SurfaceControl builder to reparent it to the root display area + * @param displayId id of the display to which root display area it should be reparented to + * @param builder surface control builder that should be updated + * @param onUpdated callback that is invoked after updating the builder, called on + * the shell main thread + */ + default void attachToRootDisplayArea(int displayId, SurfaceControl.Builder builder, + Consumer<SurfaceControl.Builder> onUpdated) { + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java new file mode 100644 index 000000000000..ef9ad6d10e6b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.displayareahelper; + +import android.view.SurfaceControl; + +import com.android.wm.shell.RootDisplayAreaOrganizer; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +public class DisplayAreaHelperController implements DisplayAreaHelper { + + private final Executor mExecutor; + private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; + + public DisplayAreaHelperController(Executor executor, + RootDisplayAreaOrganizer rootDisplayAreaOrganizer) { + mExecutor = executor; + mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer; + } + + @Override + public void attachToRootDisplayArea(int displayId, SurfaceControl.Builder builder, + Consumer<SurfaceControl.Builder> onUpdated) { + mExecutor.execute(() -> { + mRootDisplayAreaOrganizer.attachToDisplayArea(displayId, builder); + onUpdated.accept(builder); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 58bf22ad29b2..0c12d6c7bca2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -34,6 +34,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import android.content.ClipData; import android.content.ClipDescription; import android.content.Context; import android.content.res.Configuration; @@ -48,6 +49,8 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.UiEventLogger; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; @@ -67,14 +70,17 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange private final Context mContext; private final DisplayController mDisplayController; + private final DragAndDropEventLogger mLogger; private SplitScreenController mSplitScreen; private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); - public DragAndDropController(Context context, DisplayController displayController) { + public DragAndDropController(Context context, DisplayController displayController, + UiEventLogger uiEventLogger) { mContext = context; mDisplayController = displayController; + mLogger = new DragAndDropEventLogger(uiEventLogger); } public void initialize(Optional<SplitScreenController> splitscreen) { @@ -175,9 +181,10 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange Slog.w(TAG, "Unexpected drag start during an active drag"); return false; } + InstanceId loggerSessionId = mLogger.logStart(event); pd.activeDragCount++; pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), - event.getClipData()); + event.getClipData(), loggerSessionId); setDropTargetWindowVisibility(pd, View.VISIBLE); break; case ACTION_DRAG_ENTERED: @@ -198,7 +205,9 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange case ACTION_DRAG_ENDED: // TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP // or EXITED - if (!pd.dragLayout.hasDropped()) { + if (pd.dragLayout.hasDropped()) { + mLogger.logDrop(); + } else { pd.activeDragCount--; pd.dragLayout.hide(event, () -> { if (pd.activeDragCount == 0) { @@ -208,6 +217,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange } }); } + mLogger.logEnd(); break; } return true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java new file mode 100644 index 000000000000..6e4b81563441 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.draganddrop; + +import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.pm.ActivityInfo; +import android.view.DragEvent; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +/** + * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent + */ +public class DragAndDropEventLogger { + + private final UiEventLogger mUiEventLogger; + // Used to generate instance ids for this drag if one is not provided + private final InstanceIdSequence mIdSequence; + + // Tracks the current drag session + private ActivityInfo mActivityInfo; + private InstanceId mInstanceId; + + public DragAndDropEventLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); + } + + /** + * Logs the start of a drag. + */ + public InstanceId logStart(DragEvent event) { + final ClipDescription description = event.getClipDescription(); + final ClipData data = event.getClipData(); + final ClipData.Item item = data.getItemAt(0); + mInstanceId = item.getIntent().getParcelableExtra( + ClipDescription.EXTRA_LOGGING_INSTANCE_ID); + if (mInstanceId == null) { + mInstanceId = mIdSequence.newInstanceId(); + } + mActivityInfo = item.getActivityInfo(); + mUiEventLogger.logWithInstanceId(getStartEnum(description), + mActivityInfo.applicationInfo.uid, + mActivityInfo.applicationInfo.packageName, mInstanceId); + return mInstanceId; + } + + /** + * Logs a successful drop. + */ + public void logDrop() { + mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_DROPPED, + mActivityInfo.applicationInfo.uid, + mActivityInfo.applicationInfo.packageName, mInstanceId); + } + + /** + * Logs the end of a drag. + */ + public void logEnd() { + mUiEventLogger.logWithInstanceId(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_END, + mActivityInfo.applicationInfo.uid, + mActivityInfo.applicationInfo.packageName, mInstanceId); + } + + /** + * Returns the start logging enum for the given drag description. + */ + private DragAndDropUiEventEnum getStartEnum(ClipDescription description) { + if (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY)) { + return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_ACTIVITY; + } else if (description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT)) { + return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_SHORTCUT; + } else if (description.hasMimeType(MIMETYPE_APPLICATION_TASK)) { + return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_TASK; + } + throw new IllegalArgumentException("Not an app drag"); + } + + /** + * Enums for logging Drag & Drop UiEvents + */ + public enum DragAndDropUiEventEnum implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Starting a global drag and drop of an activity") + GLOBAL_APP_DRAG_START_ACTIVITY(884), + + @UiEvent(doc = "Starting a global drag and drop of a shortcut") + GLOBAL_APP_DRAG_START_SHORTCUT(885), + + @UiEvent(doc = "Starting a global drag and drop of a task") + GLOBAL_APP_DRAG_START_TASK(888), + + @UiEvent(doc = "A global app drag was successfully dropped") + GLOBAL_APP_DRAG_DROPPED(887), + + @UiEvent(doc = "Ending a global app drag and drop") + GLOBAL_APP_DRAG_END(886); + + private final int mId; + + DragAndDropUiEventEnum(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 9bcc3acf7a57..fbf04d6f3fff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -63,6 +63,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.internal.logging.InstanceId; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.split.SplitLayout.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen.StageType; @@ -86,6 +87,7 @@ public class DragAndDropPolicy { private final SplitScreenController mSplitScreen; private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); + private InstanceId mLoggerSessionId; private DragSession mSession; public DragAndDropPolicy(Context context, SplitScreenController splitScreen) { @@ -104,7 +106,8 @@ public class DragAndDropPolicy { /** * Starts a new drag session with the given initial drag data. */ - void start(DisplayLayout displayLayout, ClipData data) { + void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) { + mLoggerSessionId = loggerSessionId; mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data); // TODO(b/169894807): Also update the session data with task stack changes mSession.update(); @@ -151,10 +154,8 @@ public class DragAndDropPolicy { final Rect rightHitRegion = new Rect(); final Rect rightDrawRegion = bottomOrRightBounds; - displayRegion.splitVertically(leftHitRegion, fullscreenHitRegion, rightHitRegion); + displayRegion.splitVertically(leftHitRegion, rightHitRegion); - mTargets.add( - new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion)); @@ -165,10 +166,8 @@ public class DragAndDropPolicy { final Rect bottomDrawRegion = bottomOrRightBounds; displayRegion.splitHorizontally( - topHitRegion, fullscreenHitRegion, bottomHitRegion); + topHitRegion, bottomHitRegion); - mTargets.add( - new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion)); } @@ -211,6 +210,8 @@ public class DragAndDropPolicy { // Launch in the side stage if we are not in split-screen already. stage = STAGE_TYPE_SIDE; } + // Add some data for logging splitscreen once it is invoked + mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId); } final ClipDescription description = data.getDescription(); @@ -269,7 +270,6 @@ public class DragAndDropPolicy { * Updates the session data based on the current state of the system. */ void update() { - List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); if (!tasks.isEmpty()) { @@ -299,7 +299,12 @@ public class DragAndDropPolicy { @StageType int stage, @SplitPosition int position, @Nullable Bundle options); void enterSplitScreen(int taskId, boolean leftOrTop); - void exitSplitScreen(); + + /** + * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto + * for logging. + */ + void exitSplitScreen(int toTopTaskId, int exitTrigger); } /** @@ -352,7 +357,7 @@ public class DragAndDropPolicy { } @Override - public void exitSplitScreen() { + public void exitSplitScreen(int toTopTaskId, int exitTrigger) { throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index b3423362347f..efc9ed0f75b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -38,6 +38,7 @@ import android.view.WindowInsets.Type; import androidx.annotation.NonNull; +import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; @@ -98,8 +99,9 @@ public class DragLayout extends View { return mHasDropped; } - public void prepare(DisplayLayout displayLayout, ClipData initialData) { - mPolicy.start(displayLayout, initialData); + public void prepare(DisplayLayout displayLayout, ClipData initialData, + InstanceId loggerSessionId) { + mPolicy.start(displayLayout, initialData, loggerSessionId); mHasDropped = false; mCurrentTarget = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java index 64f7be5be813..73deea54e52f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java @@ -86,7 +86,7 @@ public class DropOutlineDrawable extends Drawable { public DropOutlineDrawable(Context context) { super(); // TODO(b/169894807): Use corner specific radii and maybe lower radius for non-edge corners - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context.getResources()); + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); mColor = context.getColor(R.color.drop_outline_background); mMaxAlpha = Color.alpha(mColor); // Initialize as hidden diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java new file mode 100644 index 000000000000..5fb3297aa6d3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.freeform; + +import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; +import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.provider.Settings; +import android.util.Slog; +import android.util.SparseArray; +import android.view.SurfaceControl; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +/** + * {@link ShellTaskOrganizer.TaskListener} for {@link + * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}. + */ +public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = "FreeformTaskListener"; + + private final SyncTransactionQueue mSyncQueue; + + private final SparseArray<State> mTasks = new SparseArray<>(); + + private static class State { + RunningTaskInfo mTaskInfo; + SurfaceControl mLeash; + } + + public FreeformTaskListener(SyncTransactionQueue syncQueue) { + mSyncQueue = syncQueue; + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mTasks.get(taskInfo.taskId) != null) { + throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d", + taskInfo.taskId); + final State state = new State(); + state.mTaskInfo = taskInfo; + state.mLeash = leash; + mTasks.put(taskInfo.taskId, state); + + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + mSyncQueue.runInSync(t -> { + Point taskPosition = taskInfo.positionInParent; + t.setPosition(leash, taskPosition.x, taskPosition.y) + .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) + .show(leash); + }); + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + State state = mTasks.get(taskInfo.taskId); + if (state == null) { + Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d", + taskInfo.taskId); + mTasks.remove(taskInfo.taskId); + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + State state = mTasks.get(taskInfo.taskId); + if (state == null) { + throw new RuntimeException( + "Task info changed before appearing: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d", + taskInfo.taskId); + state.mTaskInfo = taskInfo; + + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + final SurfaceControl leash = state.mLeash; + mSyncQueue.runInSync(t -> { + Point taskPosition = taskInfo.positionInParent; + t.setPosition(leash, taskPosition.x, taskPosition.y) + .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) + .show(leash); + }); + } + + @Override + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + mTasks.size() + " tasks"); + } + + @Override + public String toString() { + return TAG; + } + + /** + * Checks if freeform support is enabled in system. + * + * @param context context used to check settings and package manager. + * @return {@code true} if freeform is enabled, {@code false} if not. + */ + public static boolean isFreeformEnabled(Context context) { + return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) + || Settings.Global.getInt(context.getContentResolver(), + DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; + } + + /** + * Creates {@link FreeformTaskListener} if freeform is enabled. + */ + public static FreeformTaskListener create(Context context, + SyncTransactionQueue syncQueue) { + if (!isFreeformEnabled(context)) { + return null; + } + + return new FreeformTaskListener(syncQueue); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 006730d333eb..3f17f2ba9394 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -14,25 +14,31 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.fullscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; import android.graphics.Point; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.SurfaceControl; import androidx.annotation.NonNull; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.Optional; /** * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}. @@ -43,13 +49,17 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { private final SyncTransactionQueue mSyncQueue; private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>(); + private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener(); + private final FullscreenUnfoldController mFullscreenUnfoldController; - public FullscreenTaskListener(SyncTransactionQueue syncQueue) { + public FullscreenTaskListener(SyncTransactionQueue syncQueue, + Optional<FullscreenUnfoldController> unfoldController) { mSyncQueue = syncQueue; + mFullscreenUnfoldController = unfoldController.orElse(null); } @Override - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { if (mDataByTaskId.get(taskInfo.taskId) != null) { throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); } @@ -67,11 +77,16 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { t.setMatrix(leash, 1, 0, 0, 1); t.show(leash); }); + + mAnimatableTasksListener.onTaskAppeared(taskInfo); } @Override - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + + mAnimatableTasksListener.onTaskInfoChanged(taskInfo); + final TaskData data = mDataByTaskId.get(taskInfo.taskId); final Point positionInParent = taskInfo.positionInParent; if (!positionInParent.equals(data.positionInParent)) { @@ -83,12 +98,15 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { } @Override - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + public void onTaskVanished(RunningTaskInfo taskInfo) { if (mDataByTaskId.get(taskInfo.taskId) == null) { Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); return; } + + mAnimatableTasksListener.onTaskVanished(taskInfo); mDataByTaskId.remove(taskInfo.taskId); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", taskInfo.taskId); } @@ -125,4 +143,65 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { this.positionInParent = positionInParent; } } + + class AnimatableTasksListener { + private final SparseBooleanArray mTaskIds = new SparseBooleanArray(); + + public void onTaskAppeared(RunningTaskInfo taskInfo) { + final boolean isApplicable = isAnimatable(taskInfo); + if (isApplicable) { + mTaskIds.put(taskInfo.taskId, true); + + if (mFullscreenUnfoldController != null) { + SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface; + mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash); + } + } + } + + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId); + final boolean isApplicable = isAnimatable(taskInfo); + + if (isCurrentlyApplicable) { + if (isApplicable) { + // Still applicable, send update + if (mFullscreenUnfoldController != null) { + mFullscreenUnfoldController.onTaskInfoChanged(taskInfo); + } + } else { + // Became inapplicable + if (mFullscreenUnfoldController != null) { + mFullscreenUnfoldController.onTaskVanished(taskInfo); + } + mTaskIds.put(taskInfo.taskId, false); + } + } else { + if (isApplicable) { + // Became applicable + mTaskIds.put(taskInfo.taskId, true); + + if (mFullscreenUnfoldController != null) { + SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface; + mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash); + } + } + } + } + + public void onTaskVanished(RunningTaskInfo taskInfo) { + final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId); + if (isCurrentlyApplicable && mFullscreenUnfoldController != null) { + mFullscreenUnfoldController.onTaskVanished(taskInfo); + } + mTaskIds.put(taskInfo.taskId, false); + } + + private boolean isAnimatable(TaskInfo taskInfo) { + // Filter all visible tasks that are not launcher tasks + // We do not animate launcher as it handles the animation by itself + return taskInfo != null && taskInfo.isVisible && taskInfo.getConfiguration() + .windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java new file mode 100644 index 000000000000..fc1b704e95ad --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.fullscreen; + +import static android.util.MathUtils.lerp; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.concurrent.Executor; + +/** + * Controls full screen app unfold transition: animating cropping window and scaling when + * folding or unfolding a foldable device. + */ +public final class FullscreenUnfoldController implements UnfoldListener, + OnInsetsChangedListener { + + private static final float[] FLOAT_9 = new float[9]; + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + + private static final float HORIZONTAL_START_MARGIN = 0.08f; + private static final float VERTICAL_START_MARGIN = 0.03f; + private static final float END_SCALE = 1f; + private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2; + + private final Executor mExecutor; + private final ShellUnfoldProgressProvider mProgressProvider; + private final DisplayInsetsController mDisplayInsetsController; + + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final UnfoldBackgroundController mBackgroundController; + + private InsetsSource mTaskbarInsetsSource; + + private final float mWindowCornerRadiusPx; + private final float mExpandedTaskBarHeight; + + private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + + public FullscreenUnfoldController( + @NonNull Context context, + @NonNull Executor executor, + @NonNull UnfoldBackgroundController backgroundController, + @NonNull ShellUnfoldProgressProvider progressProvider, + @NonNull DisplayInsetsController displayInsetsController + ) { + mExecutor = executor; + mProgressProvider = progressProvider; + mDisplayInsetsController = displayInsetsController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + mBackgroundController = backgroundController; + } + + /** + * Initializes the controller + */ + public void init() { + mProgressProvider.addListener(mExecutor, this); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + } + + @Override + public void onStateChangeProgress(float progress) { + if (mAnimationContextByTaskId.size() == 0) return; + + mBackgroundController.ensureBackground(mTransaction); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + + context.mCurrentCropRect.set(RECT_EVALUATOR + .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); + + float scale = lerp(START_SCALE, END_SCALE, progress); + context.mMatrix.setScale(scale, scale, context.mCurrentCropRect.exactCenterX(), + context.mCurrentCropRect.exactCenterY()); + + mTransaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setMatrix(context.mLeash, context.mMatrix, FLOAT_9) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + + mTransaction.apply(); + } + + @Override + public void onStateChangeFinished() { + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(context); + } + + mBackgroundController.removeBackground(mTransaction); + mTransaction.apply(); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(mTaskbarInsetsSource, context.mTaskInfo); + } + } + + /** + * Called when a new matching task appeared + */ + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource, + taskInfo); + mAnimationContextByTaskId.put(taskInfo.taskId, animationContext); + } + + /** + * Called when matching task changed + */ + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); + if (animationContext != null) { + animationContext.update(mTaskbarInsetsSource, taskInfo); + } + } + + /** + * Called when matching task vanished + */ + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); + if (animationContext != null) { + resetSurface(animationContext); + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + if (mAnimationContextByTaskId.size() == 0) { + mBackgroundController.removeBackground(mTransaction); + } + + mTransaction.apply(); + } + + private void resetSurface(AnimationContext context) { + mTransaction + .setWindowCrop(context.mLeash, null) + .setCornerRadius(context.mLeash, 0.0F) + .setMatrix(context.mLeash, 1.0F, 0.0F, 0.0F, 1.0F) + .setPosition(context.mLeash, + (float) context.mTaskInfo.positionInParent.x, + (float) context.mTaskInfo.positionInParent.y); + } + + private class AnimationContext { + final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); + final Rect mEndCropRect = new Rect(); + final Rect mCurrentCropRect = new Rect(); + final Matrix mMatrix = new Matrix(); + + TaskInfo mTaskInfo; + + private AnimationContext(SurfaceControl leash, + InsetsSource taskBarInsetsSource, + TaskInfo taskInfo) { + this.mLeash = leash; + update(taskBarInsetsSource, taskInfo); + } + + private void update(InsetsSource taskBarInsetsSource, TaskInfo taskInfo) { + mTaskInfo = taskInfo; + mStartCropRect.set(mTaskInfo.getConfiguration().windowConfiguration.getBounds()); + + if (taskBarInsetsSource != null) { + // Only insets the cropping window with task bar when it's expanded + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mStartCropRect.inset(taskBarInsetsSource + .calculateVisibleInsets(mStartCropRect)); + } + } + + mEndCropRect.set(mStartCropRect); + + int horizontalMargin = (int) (mEndCropRect.width() * HORIZONTAL_START_MARGIN); + mStartCropRect.left = mEndCropRect.left + horizontalMargin; + mStartCropRect.right = mEndCropRect.right - horizontalMargin; + int verticalMargin = (int) (mEndCropRect.height() * VERTICAL_START_MARGIN); + mStartCropRect.top = mEndCropRect.top + verticalMargin; + mStartCropRect.bottom = mEndCropRect.bottom - verticalMargin; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java index 362b40f33e89..067f80800ed5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java @@ -20,6 +20,8 @@ import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; import static android.view.WindowManager.DOCKED_RIGHT; +import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION; import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION; @@ -100,10 +102,6 @@ public class DividerView extends FrameLayout implements OnTouchListener, private static final float MINIMIZE_DOCK_SCALE = 0f; private static final float ADJUSTED_FOR_IME_SCALE = 0.5f; - private static final PathInterpolator SLOWDOWN_INTERPOLATOR = - new PathInterpolator(0.5f, 1f, 0.5f, 1f); - private static final PathInterpolator DIM_INTERPOLATOR = - new PathInterpolator(.23f, .87f, .52f, -0.11f); private static final Interpolator IME_ADJUST_INTERPOLATOR = new PathInterpolator(0.2f, 0f, 0.1f, 1f); @@ -460,6 +458,7 @@ public class DividerView extends FrameLayout implements OnTouchListener, private void stopDragging() { mHandle.setTouching(false, true /* animate */); mWindowManager.setSlippery(true); + mWindowManagerProxy.setResizing(false); releaseBackground(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java index 40244fbb4503..f201634d3d4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java @@ -62,6 +62,7 @@ public class LegacySplitDisplayLayout { Rect mSecondary = null; Rect mAdjustedPrimary = null; Rect mAdjustedSecondary = null; + final Rect mTmpBounds = new Rect(); public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, LegacySplitScreenTaskListener taskTiles) { @@ -136,31 +137,41 @@ public class LegacySplitDisplayLayout { return mMinimizedSnapAlgorithm; } - void resizeSplits(int position) { + /** + * Resize primary bounds and secondary bounds by divider position. + * + * @param position divider position. + * @return true if calculated bounds changed. + */ + boolean resizeSplits(int position) { mPrimary = mPrimary == null ? new Rect() : mPrimary; mSecondary = mSecondary == null ? new Rect() : mSecondary; - calcSplitBounds(position, mPrimary, mSecondary); - } - - void resizeSplits(int position, WindowContainerTransaction t) { - resizeSplits(position); - t.setBounds(mTiles.mPrimary.token, mPrimary); - t.setBounds(mTiles.mSecondary.token, mSecondary); - - t.setSmallestScreenWidthDp(mTiles.mPrimary.token, - getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); - t.setSmallestScreenWidthDp(mTiles.mSecondary.token, - getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); - } - - void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) { int dockSide = getPrimarySplitSide(); - DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary, + boolean boundsChanged; + + mTmpBounds.set(mPrimary); + DockedDividerUtils.calculateBoundsForPosition(position, dockSide, mPrimary, mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); + boundsChanged = !mPrimary.equals(mTmpBounds); + mTmpBounds.set(mSecondary); DockedDividerUtils.calculateBoundsForPosition(position, - DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(), + DockedDividerUtils.invertDockSide(dockSide), mSecondary, mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); + boundsChanged |= !mSecondary.equals(mTmpBounds); + return boundsChanged; + } + + void resizeSplits(int position, WindowContainerTransaction t) { + if (resizeSplits(position)) { + t.setBounds(mTiles.mPrimary.token, mPrimary); + t.setBounds(mTiles.mSecondary.token, mSecondary); + + t.setSmallestScreenWidthDp(mTiles.mPrimary.token, + getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); + t.setSmallestScreenWidthDp(mTiles.mSecondary.token, + getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); + } } Rect calcResizableMinimizedHomeStackBounds() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java index d9409ec2dc17..b1fa2ac25fe7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java @@ -204,7 +204,8 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition != mPendingDismiss && transition != mPendingEnter) { // If we're not in split-mode, just abort @@ -239,12 +240,12 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl if (change.getParent() != null) { // This is probably reparented, so we want the parent to be immediately visible final TransitionInfo.Change parentChange = info.getChange(change.getParent()); - t.show(parentChange.getLeash()); - t.setAlpha(parentChange.getLeash(), 1.f); + startTransaction.show(parentChange.getLeash()); + startTransaction.setAlpha(parentChange.getLeash(), 1.f); // and then animate this layer outside the parent (since, for example, this is // the home task animating from fullscreen to part-screen). - t.reparent(leash, info.getRootLeash()); - t.setLayer(leash, info.getChanges().size() - i); + startTransaction.reparent(leash, info.getRootLeash()); + startTransaction.setLayer(leash, info.getChanges().size() - i); // build the finish reparent/reposition mFinishTransaction.reparent(leash, parentChange.getLeash()); mFinishTransaction.setPosition(leash, @@ -271,12 +272,12 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl if (transition == mPendingEnter && mListener.mPrimary.token.equals(change.getContainer()) || mListener.mSecondary.token.equals(change.getContainer())) { - t.setWindowCrop(leash, change.getStartAbsBounds().width(), + startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(), change.getStartAbsBounds().height()); if (mListener.mPrimary.token.equals(change.getContainer())) { // Move layer to top since we want it above the oversized home task during // animation even though home task is on top in hierarchy. - t.setLayer(leash, info.getChanges().size() + 1); + startTransaction.setLayer(leash, info.getChanges().size() + 1); } } boolean isOpening = Transitions.isOpeningType(info.getType()); @@ -289,7 +290,7 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl // Dismissing via snap-to-top/bottom means that the dismissed task is already // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 // and don't animate it so it doesn't pop-in when reparented. - t.setAlpha(leash, 0.f); + startTransaction.setAlpha(leash, 0.f); } else { startExampleAnimation(leash, false /* show */); } @@ -311,7 +312,7 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl } mSplitScreen.finishEnterSplitTransition(homeIsVisible); } - t.apply(); + startTransaction.apply(); onFinish(); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index e511bffad247..38079aff9a6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -43,6 +43,7 @@ import android.util.Slog; import android.view.Surface; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -64,7 +65,8 @@ import java.io.PrintWriter; /** * Manages and manipulates the one handed states, transitions, and gesture for phones. */ -public class OneHandedController implements RemoteCallable<OneHandedController> { +public class OneHandedController implements RemoteCallable<OneHandedController>, + DisplayChangeController.OnDisplayChangingListener { private static final String TAG = "OneHandedController"; private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = @@ -106,19 +108,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController> private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; private OneHandedUiEventLogger mOneHandedUiEventLogger; - /** - * Handle rotation based on OnDisplayChangingListener callback - */ - private final DisplayChangeController.OnDisplayChangingListener mRotationController = - (display, fromRotation, toRotation, wct) -> { - if (!isInitialized()) { - return; - } - mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct); - mOneHandedUiEventLogger.writeEvent( - OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); - }; - private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = new DisplayController.OnDisplaysChangedListener() { @Override @@ -296,7 +285,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController> getObserver(this::onSwipeToNotificationEnabledChanged); mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged); - mDisplayController.addDisplayChangingController(mRotationController); + mDisplayController.addDisplayChangingController(this); setupCallback(); registerSettingObservers(mUserId); setupTimeoutListener(); @@ -548,6 +537,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController> mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( mContext.getContentResolver(), mUserId); setSwipeToNotificationEnabled(enabled); + notifyShortcutStateChanged(mState.getState()); mOneHandedUiEventLogger.writeEvent(enabled ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHOW_NOTIFICATION_ENABLED_ON @@ -745,6 +735,27 @@ public class OneHandedController implements RemoteCallable<OneHandedController> } /** + * Handles rotation based on OnDisplayChangingListener callback + */ + @Override + public void onRotateDisplay(int displayId, int fromRotation, int toRotation, + WindowContainerTransaction wct) { + if (!isInitialized()) { + return; + } + + if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver(), + mUserId) || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + mContext.getContentResolver(), mUserId)) { + return; + } + + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct); + mOneHandedUiEventLogger.writeEvent( + OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); + } + + /** * The interface for calls from outside the Shell, within the host process. */ @ExternalThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java index c2bbd9e99bac..1b2f4768110b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -16,8 +16,6 @@ package com.android.wm.shell.onehanded; -import static android.os.UserHandle.myUserId; - import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT; import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER; @@ -186,20 +184,8 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { if (mDisplayLayout.rotation() == toRotation) { return; } - - if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(context.getContentResolver(), - myUserId())) { - return; - } - mDisplayLayout.rotateTo(context.getResources(), toRotation); updateDisplayBounds(); - - if (mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( - context.getContentResolver(), myUserId())) { - // If current settings is swipe notification, skip finishOffset. - return; - } finishOffset(0, TRANSITION_DIRECTION_EXIT); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 200af7415eb1..05111a3d4436 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -38,6 +38,7 @@ import android.view.SurfaceSession; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.transition.Transitions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -617,14 +618,28 @@ public class PipAnimationController { setCurrentValue(bounds); final Rect insets = computeInsets(fraction); final float degree, x, y; - if (rotationDelta == ROTATION_90) { - degree = 90 * fraction; - x = fraction * (end.right - start.left) + start.left; - y = fraction * (end.top - start.top) + start.top; + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + if (rotationDelta == ROTATION_90) { + degree = 90 * (1 - fraction); + x = fraction * (end.left - start.left) + + start.left + start.right * (1 - fraction); + y = fraction * (end.top - start.top) + start.top; + } else { + degree = -90 * (1 - fraction); + x = fraction * (end.left - start.left) + start.left; + y = fraction * (end.top - start.top) + + start.top + start.bottom * (1 - fraction); + } } else { - degree = -90 * fraction; - x = fraction * (end.left - start.left) + start.left; - y = fraction * (end.bottom - start.top) + start.top; + if (rotationDelta == ROTATION_90) { + degree = 90 * fraction; + x = fraction * (end.right - start.left) + start.left; + y = fraction * (end.top - start.top) + start.top; + } else { + degree = -90 * fraction; + x = fraction * (end.left - start.left) + start.left; + y = fraction * (end.bottom - start.top) + start.top; + } } getSurfaceTransactionHelper() .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index 728794de0865..180e3fb48c9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -23,6 +23,7 @@ import android.graphics.RectF; import android.view.SurfaceControl; import com.android.wm.shell.R; +import com.android.wm.shell.transition.Transitions; /** * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. @@ -137,7 +138,8 @@ public class PipSurfaceTransactionHelper { // destination are different. final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH; final Rect crop = mTmpDestinationRect; - crop.set(0, 0, destW, destH); + crop.set(0, 0, Transitions.ENABLE_SHELL_TRANSITIONS ? destH + : destW, Transitions.ENABLE_SHELL_TRANSITIONS ? destW : destH); // Inverse scale for crop to fit in screen coordinates. crop.scale(1 / scale); crop.offset(insets.left, insets.top); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index f2bad6caf3e8..291cbb3676dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -114,38 +114,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500; - // Not a complete set of states but serves what we want right now. - private enum State { - UNDEFINED(0), - TASK_APPEARED(1), - ENTRY_SCHEDULED(2), - ENTERING_PIP(3), - ENTERED_PIP(4), - EXITING_PIP(5); - - private final int mStateValue; - - State(int value) { - mStateValue = value; - } - - private boolean isInPip() { - return mStateValue >= TASK_APPEARED.mStateValue - && mStateValue != EXITING_PIP.mStateValue; - } - - /** - * Resize request can be initiated in other component, ignore if we are no longer in PIP, - * still waiting for animation or we're exiting from it. - * - * @return {@code true} if the resize request should be blocked/ignored. - */ - private boolean shouldBlockResizeRequest() { - return mStateValue < ENTERING_PIP.mStateValue - || mStateValue == EXITING_PIP.mStateValue; - } - } - private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; private final PipBoundsState mPipBoundsState; @@ -169,11 +137,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onPipAnimationStart(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // TODO (b//169221267): Add jank listener for transactions without buffer updates. - //InteractionJankMonitor.getInstance().begin( - // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); - } sendOnPipTransitionStarted(direction); } @@ -201,7 +164,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } final boolean isExitPipDirection = isOutPipDirection(direction) || isRemovePipDirection(direction); - if (mState != State.EXITING_PIP || isExitPipDirection) { + if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP + || isExitPipDirection) { // Finish resize as long as we're not exiting PIP, or, if we are, only if this is // the end of an exit PIP animation. // This is necessary in case there was a resize animation ongoing when exit PIP @@ -244,7 +208,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private ActivityManager.RunningTaskInfo mDeferredTaskInfo; private WindowContainerToken mToken; private SurfaceControl mLeash; - private State mState = State.UNDEFINED; + private PipTransitionState mPipTransitionState; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private long mLastOneShotAlphaAnimationTime; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory @@ -274,21 +238,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private @Surface.Rotation int mCurrentRotation; /** - * If set to {@code true}, no entering PiP transition would be kicked off and most likely - * it's due to the fact that Launcher is handling the transition directly when swiping - * auto PiP-able Activity to home. - * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}. - */ - private boolean mInSwipePipToHomeTransition; - - /** * An optional overlay used to mask content changing between an app in/out of PiP, only set if - * {@link #mInSwipePipToHomeTransition} is true. + * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true. */ private SurfaceControl mSwipePipToHomeOverlay; public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, + @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @@ -302,6 +259,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @ShellMainThread ShellExecutor mainExecutor) { mContext = context; mSyncTransactionQueue = syncTransactionQueue; + mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; @@ -324,6 +282,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mMainExecutor.execute(() -> { mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); }); + mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); } @@ -337,14 +296,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } public boolean isInPip() { - return mState.isInPip(); + return mPipTransitionState.isInPip(); } /** * Returns whether the entry animation is waiting to be started. */ public boolean isEntryScheduled() { - return mState == State.ENTRY_SCHEDULED; + return mPipTransitionState.getTransitionState() == PipTransitionState.ENTRY_SCHEDULED; } /** @@ -372,7 +331,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams) { - mInSwipePipToHomeTransition = true; + mPipTransitionState.setInSwipePipToHomeTransition(true); sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); return mPipBoundsAlgorithm.getEntryDestinationBounds(); @@ -385,12 +344,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, SurfaceControl overlay) { // do nothing if there is no startSwipePipToHome being called before - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { mPipBoundsState.setBounds(destinationBounds); mSwipePipToHomeOverlay = overlay; } } + public ActivityManager.RunningTaskInfo getTaskInfo() { + return mTaskInfo; + } + public SurfaceControl getSurfaceControl() { return mLeash; } @@ -412,9 +375,11 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * @param animationDurationMs duration in millisecond for the exiting PiP transition */ public void exitPip(int animationDurationMs) { - if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) { + if (!mPipTransitionState.isInPip() + || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP + || mToken == null) { Log.wtf(TAG, "Not allowed to exitPip in current state" - + " mState=" + mState + " mToken=" + mToken); + + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken); return; } @@ -438,7 +403,12 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, wct.setBoundsChangeTransaction(mToken, tx); // Set the exiting state first so if there is fixed rotation later, the running animation // won't be interrupted by alpha animation for existing PiP. - mState = State.EXITING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mPipTransitionController.startTransition(destinationBounds, wct); + return; + } mSyncTransactionQueue.queue(wct); mSyncTransactionQueue.runInSync(t -> { // Make sure to grab the latest source hint rect as it could have been @@ -476,9 +446,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Removes PiP immediately. */ public void removePip() { - if (!mState.isInPip() || mToken == null) { + if (!mPipTransitionState.isInPip() || mToken == null) { Log.wtf(TAG, "Not allowed to removePip in current state" - + " mState=" + mState + " mToken=" + mToken); + + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken); return; } @@ -492,10 +462,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, animator.setDuration(mExitAnimationDuration); animator.setInterpolator(Interpolators.ALPHA_OUT); animator.start(); - mState = State.EXITING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); } private void removePipImmediately() { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mToken, null); + wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.reorder(mToken, false); + mPipTransitionController.startTransition(null, wct); + return; + } + try { // Reset the task bounds first to ensure the activity configuration is reset as well final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -514,7 +493,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Objects.requireNonNull(info, "Requires RunningTaskInfo"); mTaskInfo = info; mToken = mTaskInfo.token; - mState = State.TASK_APPEARED; + mPipTransitionState.setTransitionState(PipTransitionState.TASK_APPEARED); mLeash = leash; mPictureInPictureParams = mTaskInfo.pictureInPictureParams; setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, @@ -530,7 +509,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mOnDisplayIdChangeCallback.accept(info.displayId); } - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); } else { @@ -557,6 +536,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (Transitions.ENABLE_SHELL_TRANSITIONS) { if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { mPipMenuController.attach(mLeash); + } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + mOneShotAnimationType = ANIM_TYPE_BOUNDS; } return; } @@ -568,7 +549,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, null /* updateBoundsCallback */); - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -595,7 +576,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); animateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */); - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } /** @@ -620,7 +601,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceControlTransactionFactory.getTransaction(); tx.setAlpha(mLeash, 0f); tx.apply(); - mState = State.ENTRY_SCHEDULED; + mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); applyEnterPipSyncTransaction(destinationBounds, () -> { mPipAnimationController .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f) @@ -631,11 +612,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, .start(); // mState is set right after the animation is kicked off to block any resize // requests such as offsetPip that may have been called prior to the transition. - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); }, null /* boundsChangeTransaction */); } private void onEndOfSwipePipToHomeTransition() { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mSwipePipToHomeOverlay = null; + return; + } + final Rect destinationBounds = mPipBoundsState.getBounds(); final SurfaceControl swipeToHomeOverlay = mSwipePipToHomeOverlay; final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); @@ -655,7 +641,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, null /* callback */, false /* withStartDelay */); } }, tx); - mInSwipePipToHomeTransition = false; + mPipTransitionState.setInSwipePipToHomeTransition(false); mSwipePipToHomeOverlay = null; } @@ -679,7 +665,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } mPipTransitionController.sendOnPipTransitionStarted(direction); } @@ -688,7 +674,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { - mState = State.ENTERED_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); } mPipTransitionController.sendOnPipTransitionFinished(direction); // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent. @@ -713,7 +699,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ @Override public void onTaskVanished(ActivityManager.RunningTaskInfo info) { - if (mState == State.UNDEFINED) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { return; } final WindowContainerToken token = info.token; @@ -723,9 +709,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } clearWaitForFixedRotation(); - mInSwipePipToHomeTransition = false; + mPipTransitionState.setInSwipePipToHomeTransition(false); mPictureInPictureParams = null; - mState = State.UNDEFINED; + mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); // Re-set the PIP bounds to none. mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); @@ -735,6 +721,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mPipTransitionController.forceFinishTransition(); + } final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController.getCurrentAnimator(); if (animator != null) { @@ -750,8 +739,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); - if (mState != State.ENTERED_PIP && mState != State.EXITING_PIP) { - Log.d(TAG, "Defer onTaskInfoChange in current state: " + mState); + if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP + && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) { + Log.d(TAG, "Defer onTaskInfoChange in current state: " + + mPipTransitionState.getTransitionState()); // Defer applying PiP parameters if the task is entering PiP to avoid disturbing // the animation. mDeferredTaskInfo = info; @@ -784,7 +775,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mNextRotation = newRotation; mWaitForFixedRotation = true; - if (mState.isInPip()) { + if (mPipTransitionState.isInPip()) { // Fade out the existing PiP to avoid jump cut during seamless rotation. fadeExistingPip(false /* show */); } @@ -795,17 +786,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (!mWaitForFixedRotation) { return; } - if (mState == State.TASK_APPEARED) { - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { onEndOfSwipePipToHomeTransition(); } else { // Schedule a regular animation to ensure all the callbacks are still being sent. enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(), mEnterAnimationDuration); } - } else if (mState == State.ENTERED_PIP && mHasFadeOut) { + } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERED_PIP + && mHasFadeOut) { fadeExistingPip(true /* show */); - } else if (mState == State.ENTERING_PIP && mDeferredAnimEndTransaction != null) { + } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERING_PIP + && mDeferredAnimEndTransaction != null) { final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController.getCurrentAnimator(); final Rect destinationBounds = animator.getDestinationBounds(); @@ -859,13 +852,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // note that this can be called when swipe-to-home or fixed-rotation is happening. // Skip this entirely if that's the case. final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation - && (mState != State.ENTERED_PIP); - if ((mInSwipePipToHomeTransition || waitForFixedRotationOnEnteringPip) && fromRotation) { + && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP); + if ((mPipTransitionState.getInSwipePipToHomeTransition() + || waitForFixedRotationOnEnteringPip) && fromRotation) { if (DEBUG) { Log.d(TAG, "Skip onMovementBoundsChanged on rotation change" - + " mInSwipePipToHomeTransition=" + mInSwipePipToHomeTransition + + " InSwipePipToHomeTransition=" + + mPipTransitionState.getInSwipePipToHomeTransition() + " mWaitForFixedRotation=" + mWaitForFixedRotation - + " mState=" + mState); + + " getTransitionState=" + mPipTransitionState.getTransitionState()); } return; } @@ -873,7 +868,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController.getCurrentAnimator(); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { - final boolean rotatingPip = mState.isInPip() && fromRotation; + final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation; if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { // The position will be used by fade-in animation when the fixed rotation is done. mPipBoundsState.setBounds(destinationBoundsOut); @@ -1006,7 +1001,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback) { - if (!mState.isInPip()) { + if (!mPipTransitionState.isInPip()) { // TODO: tend to use shouldBlockResizeRequest here as well but need to consider // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window // container transaction callback and we want to set the mState immediately. @@ -1036,7 +1031,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, toBounds) - .round(tx, mLeash, mState.isInPip()); + .round(tx, mLeash, mPipTransitionState.isInPip()); if (mPipMenuController.isMenuVisible()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); } else { @@ -1114,7 +1109,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback) { - if (mState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest()) { return; } @@ -1131,7 +1126,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) - .round(tx, mLeash, mState.isInPip()); + .round(tx, mLeash, mPipTransitionState.isInPip()); return tx; } @@ -1140,7 +1135,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { - if (mState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest()) { return; } if (mWaitForFixedRotation) { @@ -1384,7 +1379,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f); animator.setDuration(mCrossFadeAnimationDuration); animator.addUpdateListener(animation -> { - if (mState == State.UNDEFINED) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Could happen if onTaskVanished happens during the animation since we may have // set a start delay on this animation. Log.d(TAG, "Task vanished, skip fadeOutAndRemoveOverlay"); @@ -1410,7 +1405,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } private void removeContentOverlay(SurfaceControl surface, Runnable callback) { - if (mState == State.UNDEFINED) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Avoid double removal, which is fatal. return; } @@ -1432,7 +1427,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, pw.println(innerPrefix + "mToken=" + mToken + " binder=" + (mToken != null ? mToken.asBinder() : null)); pw.println(innerPrefix + "mLeash=" + mLeash); - pw.println(innerPrefix + "mState=" + mState); + pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState()); pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 4759550c35c0..328f3ed73f2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -18,6 +18,10 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.util.RotationUtils.deltaRotation; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PIP; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; @@ -25,11 +29,16 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; +import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; import android.os.IBinder; +import android.util.Log; import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -49,74 +58,258 @@ import com.android.wm.shell.transition.Transitions; */ public class PipTransition extends PipTransitionController { + private static final String TAG = PipTransition.class.getSimpleName(); + + private final PipTransitionState mPipTransitionState; private final int mEnterExitAnimationDuration; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private Transitions.TransitionFinishCallback mFinishCallback; + private Rect mExitDestinationBounds = new Rect(); + private IBinder mExitTransition = null; public PipTransition(Context context, - PipBoundsState pipBoundsState, PipMenuController pipMenuController, + PipBoundsState pipBoundsState, + PipTransitionState pipTransitionState, + PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @NonNull ShellTaskOrganizer shellTaskOrganizer) { super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + mPipTransitionState = pipTransitionState; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); } @Override + public void setIsFullAnimation(boolean isFullAnimation) { + setOneShotAnimationType(isFullAnimation ? ANIM_TYPE_BOUNDS : ANIM_TYPE_ALPHA); + } + + /** + * Sets the preferred animation type for one time. + * This is typically used to set the animation type to + * {@link PipAnimationController#ANIM_TYPE_ALPHA}. + */ + private void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { + mOneShotAnimationType = animationType; + } + + @Override + public void startTransition(Rect destinationBounds, WindowContainerTransaction out) { + if (destinationBounds != null) { + mExitDestinationBounds.set(destinationBounds); + mExitTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); + } else { + mTransitions.startTransition(TRANSIT_REMOVE_PIP, out, this); + } + } + + @Override public boolean startAnimation(@android.annotation.NonNull IBinder transition, @android.annotation.NonNull TransitionInfo info, - @android.annotation.NonNull SurfaceControl.Transaction t, + @android.annotation.NonNull SurfaceControl.Transaction startTransaction, + @android.annotation.NonNull SurfaceControl.Transaction finishTransaction, @android.annotation.NonNull Transitions.TransitionFinishCallback finishCallback) { + + if (mExitTransition == transition || info.getType() == TRANSIT_EXIT_PIP) { + mExitTransition = null; + if (info.getChanges().size() == 1) { + final TransitionInfo.Change change = info.getChanges().get(0); + mFinishCallback = finishCallback; + startTransaction.apply(); + boolean success = startExpandAnimation(change.getTaskInfo(), change.getLeash(), + new Rect(mExitDestinationBounds)); + mExitDestinationBounds.setEmpty(); + return success; + } else { + Log.e(TAG, "Got an exit-pip transition with unexpected change-list"); + } + } + + if (info.getType() == TRANSIT_REMOVE_PIP) { + startTransaction.apply(); + finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), + mPipBoundsState.getDisplayBounds()); + finishCallback.onTransitionFinished(null, null); + return true; + } + + // We only support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps + // that enter PiP instantly on opening, mostly from CTS/Flicker tests) + if (info.getType() != TRANSIT_PIP && info.getType() != TRANSIT_OPEN) { + return false; + } + + // Search for an Enter PiP transition (along with a show wallpaper one) + TransitionInfo.Change enterPip = null; + TransitionInfo.Change wallpaper = null; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change.getTaskInfo() != null && change.getTaskInfo().configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED) { - mFinishCallback = finishCallback; - return startEnterAnimation(change.getTaskInfo(), change.getLeash(), t); + enterPip = change; + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + wallpaper = change; } } - return false; + if (enterPip == null) { + return false; + } + + // Show the wallpaper if there is a wallpaper change. + if (wallpaper != null) { + startTransaction.show(wallpaper.getLeash()); + startTransaction.setAlpha(wallpaper.getLeash(), 1.f); + } + + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); + mFinishCallback = finishCallback; + return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(), + startTransaction, finishTransaction, enterPip.getStartRotation(), + enterPip.getEndRotation()); } @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - return null; + if (request.getType() == TRANSIT_PIP) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); + if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + wct.setActivityWindowingMode(request.getTriggerTask().token, + WINDOWING_MODE_UNDEFINED); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + wct.setBounds(request.getTriggerTask().token, destinationBounds); + } + return wct; + } else { + return null; + } + } + + @Override + public void onTransitionMerged(@NonNull IBinder transition) { + if (transition != mExitTransition) { + return; + } + // This means an expand happened before enter-pip finished and we are now "merging" a + // no-op transition that happens to match our exit-pip. + boolean cancelled = false; + if (mPipAnimationController.getCurrentAnimator() != null) { + mPipAnimationController.getCurrentAnimator().cancel(); + cancelled = true; + } + // Unset exitTransition AFTER cancel so that finishResize knows we are merging. + mExitTransition = null; + if (!cancelled) return; + final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo(); + if (taskInfo != null) { + startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(), + new Rect(mExitDestinationBounds)); + } + mExitDestinationBounds.setEmpty(); } @Override public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, - SurfaceControl.Transaction tx) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareFinishResizeTransaction(taskInfo, destinationBounds, - direction, tx, wct); - mFinishCallback.onTransitionFinished(wct, null); + @Nullable SurfaceControl.Transaction tx) { + + if (isInPipDirection(direction)) { + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); + } + // If there is an expected exit transition, then the exit will be "merged" into this + // transition so don't fire the finish-callback in that case. + if (mExitTransition == null && mFinishCallback != null) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareFinishResizeTransaction(taskInfo, destinationBounds, + direction, wct); + if (tx != null) { + wct.setBoundsChangeTransaction(taskInfo.token, tx); + } + mFinishCallback.onTransitionFinished(wct, null /* wctCallback */); + mFinishCallback = null; + } finishResizeForMenu(destinationBounds); } + @Override + public void forceFinishTransition() { + if (mFinishCallback == null) return; + mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCallback */); + mFinishCallback = null; + } + + private boolean startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash, + final Rect destinationBounds) { + PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), + mPipBoundsState.getBounds(), destinationBounds, null, + TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, Surface.ROTATION_0); + + animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start(); + + return true; + } + private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash, - final SurfaceControl.Transaction t) { + final SurfaceControl.Transaction startTransaction, + final SurfaceControl.Transaction finishTransaction, + final int startRotation, final int endRotation) { setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, taskInfo.topActivityInfo); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds(); PipAnimationController.PipTransitionAnimator animator; + finishTransaction.setPosition(leash, destinationBounds.left, destinationBounds.top); + if (taskInfo.pictureInPictureParams != null + && taskInfo.pictureInPictureParams.isAutoEnterEnabled() + && mPipTransitionState.getInSwipePipToHomeTransition()) { + mOneShotAnimationType = ANIM_TYPE_BOUNDS; + + // PiP menu is attached late in the process here to avoid any artifacts on the leash + // caused by addShellRoot when in gesture navigation mode. + mPipMenuController.attach(leash); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9]) + .setPosition(leash, destinationBounds.left, destinationBounds.top) + .setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()); + startTransaction.merge(tx); + startTransaction.apply(); + mPipBoundsState.setBounds(destinationBounds); + onFinishResize(taskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, null /* tx */); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + mFinishCallback = null; + mPipTransitionState.setInSwipePipToHomeTransition(false); + return true; + } + + int rotationDelta = deltaRotation(endRotation, startRotation); + if (rotationDelta != Surface.ROTATION_0) { + Matrix tmpTransform = new Matrix(); + tmpTransform.postRotate(rotationDelta == Surface.ROTATION_90 + ? Surface.ROTATION_270 : Surface.ROTATION_90); + startTransaction.setMatrix(leash, tmpTransform, new float[9]); + } if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( taskInfo.pictureInPictureParams, currentBounds); animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, Surface.ROTATION_0); + 0 /* startingAngle */, rotationDelta); } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - t.setAlpha(leash, 0f); - t.apply(); + startTransaction.setAlpha(leash, 0f); + // PiP menu is attached late in the process here to avoid any artifacts on the leash + // caused by addShellRoot when in gesture navigation mode. + mPipMenuController.attach(leash); animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds, 0f, 1f); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -124,10 +317,12 @@ public class PipTransition extends PipTransitionController { throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); } + startTransaction.apply(); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) .start(); + return true; } @@ -138,7 +333,6 @@ public class PipTransition extends PipTransitionController { private void prepareFinishResizeTransaction(TaskInfo taskInfo, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, - SurfaceControl.Transaction tx, WindowContainerTransaction wct) { Rect taskBounds = null; if (isInPipDirection(direction)) { @@ -158,6 +352,5 @@ public class PipTransition extends PipTransitionController { } wct.setBounds(taskInfo.token, taskBounds); - wct.setBoundsChangeTransaction(taskInfo.token, tx); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index d801c918973a..376f3298a83c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -19,7 +19,6 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; -import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import android.app.PictureInPictureParams; import android.app.TaskInfo; @@ -29,6 +28,7 @@ import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.transition.Transitions; @@ -46,8 +46,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final PipBoundsState mPipBoundsState; protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; + protected final Transitions mTransitions; private final Handler mMainHandler; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + protected PipTaskOrganizer mPipOrganizer; protected final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { @@ -55,12 +57,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void onPipAnimationStart(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // TODO (b//169221267): Add jank listener for transactions without buffer - // updates. - //InteractionJankMonitor.getInstance().begin( - // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); - } sendOnPipTransitionStarted(direction); } @@ -74,12 +70,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH } onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx); sendOnPipTransitionFinished(direction); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // TODO (b//169221267): Add jank listener for transactions without buffer - // updates. - //InteractionJankMonitor.getInstance().end( - // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP); - } } @Override @@ -98,6 +88,29 @@ public abstract class PipTransitionController implements Transitions.TransitionH SurfaceControl.Transaction tx) { } + /** + * Called to inform the transition that the animation should start with the assumption that + * PiP is not animating from its original bounds, but rather a continuation of another + * animation. For example, gesture navigation would first fade out the PiP activity, and the + * transition should be responsible to animate in (such as fade in) the PiP. + */ + public void setIsFullAnimation(boolean isFullAnimation) { + } + + /** + * Called when the Shell wants to starts a transition/animation. + */ + public void startTransition(Rect destinationBounds, WindowContainerTransaction out) { + // Default implementation does nothing. + } + + /** + * Called when the transition animation can't continue (eg. task is removed during + * animation) + */ + public void forceFinishTransition() { + } + public PipTransitionController(PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @@ -107,12 +120,17 @@ public abstract class PipTransitionController implements Transitions.TransitionH mShellTaskOrganizer = shellTaskOrganizer; mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipAnimationController = pipAnimationController; + mTransitions = transitions; mMainHandler = new Handler(Looper.getMainLooper()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.addHandler(this); } } + void setPipOrganizer(PipTaskOrganizer pto) { + mPipOrganizer = pto; + } + /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java new file mode 100644 index 000000000000..85e56b7dd99f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip; + +import android.annotation.IntDef; +import android.app.PictureInPictureParams; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and + * {@link PipTransition}. + */ +public class PipTransitionState { + + public static final int UNDEFINED = 0; + public static final int TASK_APPEARED = 1; + public static final int ENTRY_SCHEDULED = 2; + public static final int ENTERING_PIP = 3; + public static final int ENTERED_PIP = 4; + public static final int EXITING_PIP = 5; + + /** + * If set to {@code true}, no entering PiP transition would be kicked off and most likely + * it's due to the fact that Launcher is handling the transition directly when swiping + * auto PiP-able Activity to home. + * See also {@link PipTaskOrganizer#startSwipePipToHome(ComponentName, ActivityInfo, + * PictureInPictureParams)}. + */ + private boolean mInSwipePipToHomeTransition; + + // Not a complete set of states but serves what we want right now. + @IntDef(prefix = { "TRANSITION_STATE_" }, value = { + UNDEFINED, + TASK_APPEARED, + ENTRY_SCHEDULED, + ENTERING_PIP, + ENTERED_PIP, + EXITING_PIP + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionState {} + + private @TransitionState int mState; + + public PipTransitionState() { + mState = UNDEFINED; + } + + public void setTransitionState(@TransitionState int state) { + mState = state; + } + + public @TransitionState int getTransitionState() { + return mState; + } + + public boolean isInPip() { + return mState >= TASK_APPEARED + && mState != EXITING_PIP; + } + + public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { + mInSwipePipToHomeTransition = inSwipePipToHomeTransition; + } + + public boolean getInSwipePipToHomeTransition() { + return mInSwipePipToHomeTransition; + } + /** + * Resize request can be initiated in other component, ignore if we are no longer in PIP, + * still waiting for animation or we're exiting from it. + * + * @return {@code true} if the resize request should be blocked/ignored. + */ + public boolean shouldBlockResizeRequest() { + return mState < ENTERING_PIP + || mState == EXITING_PIP; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index a646b07c49dc..ae8c1b6f8c1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -60,8 +60,7 @@ public class PhonePipMenuController implements PipMenuController { private static final boolean DEBUG = false; public static final int MENU_STATE_NONE = 0; - public static final int MENU_STATE_CLOSE = 1; - public static final int MENU_STATE_FULL = 2; + public static final int MENU_STATE_FULL = 1; /** * A listener interface to receive notification on changes in PIP. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 63f1985aa86e..8e5c5c52cb3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -21,7 +21,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static android.view.WindowManager.INPUT_CONSUMER_PIP; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; import android.app.ActivityManager; @@ -52,6 +61,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayChangeController; @@ -67,6 +77,7 @@ import com.android.wm.shell.pip.IPip; import com.android.wm.shell.pip.IPipAnimationListener; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; @@ -74,6 +85,7 @@ import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.Objects; @@ -445,11 +457,18 @@ public class PipController implements PipTransitionController.PipTransitionCallb return; } Runnable updateDisplayLayout = () -> { + final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS + && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation(); mPipBoundsState.setDisplayLayout(layout); + final WindowContainerTransaction wct = + fromRotation ? new WindowContainerTransaction() : null; updateMovementBounds(null /* toBounds */, - false /* fromRotation */, false /* fromImeAdjustment */, + fromRotation, false /* fromImeAdjustment */, false /* fromShelfAdjustment */, - null /* windowContainerTransaction */); + wct /* windowContainerTransaction */); + if (wct != null) { + mPipTaskOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_SAME); + } }; if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { @@ -528,6 +547,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void setPinnedStackAnimationType(int animationType) { mPipTaskOrganizer.setOneShotAnimationType(animationType); + mPipTransitionController.setIsFullAnimation( + animationType == PipAnimationController.ANIM_TYPE_BOUNDS); } private void setPinnedStackAnimationListener(IPipAnimationListener callback) { @@ -564,8 +585,37 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds, overlay); } + private String getTransitionTag(int direction) { + switch (direction) { + case TRANSITION_DIRECTION_TO_PIP: + return "TRANSITION_TO_PIP"; + case TRANSITION_DIRECTION_LEAVE_PIP: + return "TRANSITION_LEAVE_PIP"; + case TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN: + return "TRANSITION_LEAVE_PIP_TO_SPLIT_SCREEN"; + case TRANSITION_DIRECTION_REMOVE_STACK: + return "TRANSITION_REMOVE_STACK"; + case TRANSITION_DIRECTION_SNAP_AFTER_RESIZE: + return "TRANSITION_SNAP_AFTER_RESIZE"; + case TRANSITION_DIRECTION_USER_RESIZE: + return "TRANSITION_USER_RESIZE"; + case TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND: + return "TRANSITION_EXPAND_OR_UNEXPAND"; + default: + return "TRANSITION_LEAVE_UNKNOWN"; + } + } + @Override public void onPipTransitionStarted(int direction, Rect pipBounds) { + // Begin InteractionJankMonitor with PIP transition CUJs + final InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withSurface( + CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl()) + .setTag(getTransitionTag(direction)) + .setTimeout(2000); + InteractionJankMonitor.getInstance().begin(builder); + if (isOutPipDirection(direction)) { // Exiting PIP, save the reentry state to restore to when re-entering. saveReentryState(pipBounds); @@ -604,6 +654,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void onPipTransitionFinishedOrCanceled(int direction) { + // End InteractionJankMonitor with PIP transition by CUJs + InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION); + // Re-enable touches after the animation completes mTouchHandler.setTouchEnabled(true); mTouchHandler.onPinnedStackAnimationEnded(direction); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java index 1da9577fe49a..0fbdf90fd9d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.content.Context; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; @@ -30,6 +31,7 @@ import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; @@ -93,6 +95,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen private int mTargetSize; private int mDismissAreaHeight; private float mMagneticFieldRadiusPercent = 1f; + private WindowInsets mWindowInsets; private SurfaceControl mTaskLeash; private boolean mHasDismissTargetSurface; @@ -123,6 +126,13 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen mContext.getDrawable(R.drawable.floating_dismiss_gradient_transition)); mTargetViewContainer.setClipChildren(false); mTargetViewContainer.addView(mTargetView); + mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + updateMagneticTargetSize(); + } + return windowInsets; + }); mMagnetizedPip = mMotionHelper.getMagnetizedPip(); mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); @@ -158,14 +168,16 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mMainExecutor.executeDelayed(() -> { - mMotionHelper.notifyDismissalPending(); - mMotionHelper.animateDismiss(); - hideDismissTargetMaybe(); - - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); - }, 0); + if (mEnableDismissDragToEdge) { + mMainExecutor.executeDelayed(() -> { + mMotionHelper.notifyDismissalPending(); + mMotionHelper.animateDismiss(); + hideDismissTargetMaybe(); + + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); + }, 0); + } } }); @@ -199,10 +211,13 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen final Resources res = mContext.getResources(); mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + final WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + final Insets navInset = insets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars()); final FrameLayout.LayoutParams newParams = new FrameLayout.LayoutParams(mTargetSize, mTargetSize); newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; - newParams.bottomMargin = mContext.getResources().getDimensionPixelSize( + newParams.bottomMargin = navInset.bottom + mContext.getResources().getDimensionPixelSize( R.dimen.floating_dismiss_bottom_margin); mTargetView.setLayoutParams(newParams); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index 67b1e6dd4cc7..8ef2b6b12030 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -23,7 +23,6 @@ import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTR import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; -import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; @@ -203,7 +202,7 @@ public class PipMenuView extends FrameLayout { @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) { + if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) { mController.showMenu(); } return super.performAccessibilityAction(host, action, args); @@ -271,13 +270,12 @@ public class PipMenuView extends FrameLayout { mDismissButton.getAlpha(), 1f); ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, mResizeHandle.getAlpha(), - ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle - ? 1f : 0f); + ENABLE_RESIZE_HANDLE && showResizeHandle ? 1f : 0f); if (menuState == MENU_STATE_FULL) { mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim); } else { - mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim); + mMenuContainerAnimator.playTogether(resizeAnim); } mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS); @@ -429,7 +427,7 @@ public class PipMenuView extends FrameLayout { FrameLayout.LayoutParams expandedLp = (FrameLayout.LayoutParams) expandContainer.getLayoutParams(); - if (mActions.isEmpty() || menuState == MENU_STATE_CLOSE || menuState == MENU_STATE_NONE) { + if (mActions.isEmpty() || menuState == MENU_STATE_NONE) { actionsContainer.setVisibility(View.INVISIBLE); // Update the expand container margin to adjust the center of the expand button to diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 7867f933de4f..9f2f6a575aca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -22,7 +22,6 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT; import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; -import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; @@ -81,7 +80,6 @@ public class PipTouchHandler { private final PhonePipMenuController mMenuController; private final AccessibilityManager mAccessibilityManager; - private boolean mShowPipMenuOnAnimationEnd = false; /** * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the @@ -280,7 +278,6 @@ public class PipTouchHandler { public void onActivityPinned() { mPipDismissTargetHandler.createOrUpdateDismissTarget(); - mShowPipMenuOnAnimationEnd = true; mPipResizeGestureHandler.onActivityPinned(); mFloatingContentCoordinator.onContentAdded(mMotionHelper); } @@ -304,13 +301,6 @@ public class PipTouchHandler { // Set the initial bounds as the user resize bounds. mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); } - - if (mShowPipMenuOnAnimationEnd) { - mMenuController.showMenu(MENU_STATE_CLOSE, mPipBoundsState.getBounds(), - true /* allowMenuTimeout */, false /* willResizeMenu */, - shouldShowResizeHandle()); - mShowPipMenuOnAnimationEnd = false; - } } public void onConfigurationChanged() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index b7caf72641a3..551476dc9d54 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -58,7 +58,8 @@ public class TvPipTransition extends PipTransitionController { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @android.annotation.NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java index 1fc4d12def1f..ab3cbd655ea1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java @@ -47,6 +47,8 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang /** Callback for size compat UI interaction. */ public interface SizeCompatUICallback { + /** Called when the size compat restart button appears. */ + void onSizeCompatRestartButtonAppeared(int taskId); /** Called when the size compat restart button is clicked. */ void onSizeCompatRestartButtonClicked(int taskId); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java index 20021ebea834..7cf95593dbaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java @@ -106,6 +106,8 @@ class SizeCompatUILayout { mShouldShowHint = false; createSizeCompatHint(); } + + mCallback.onSizeCompatRestartButtonAppeared(mTaskId); } /** Creates the restart button hint window. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index 8f0892fdcbba..3d3a63057dde 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -20,7 +20,9 @@ import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; -import android.window.IRemoteTransition; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.window.RemoteTransition; import com.android.wm.shell.splitscreen.ISplitScreenListener; @@ -50,9 +52,10 @@ interface ISplitScreen { oneway void removeFromSideStage(int taskId) = 4; /** - * Removes the split-screen stages. + * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID + * to indicate leaving no top task after leaving split-screen. */ - oneway void exitSplitScreen() = 5; + oneway void exitSplitScreen(int toTopTaskId) = 5; /** * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible. @@ -77,9 +80,24 @@ interface ISplitScreen { int position, in Bundle options) = 9; /** - * Starts tasks simultaneously in one transition. The first task in the list will be in the - * main-stage and on the left/top. + * Starts tasks simultaneously in one transition. */ oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, - in Bundle sideOptions, int sidePosition, in IRemoteTransition remoteTransition) = 10; + in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10; + + /** + * Version of startTasks using legacy transition system. + */ + oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, + int sideTaskId, in Bundle sideOptions, int sidePosition, + in RemoteAnimationAdapter adapter) = 11; + + /** + * Blocking call that notifies and gets additional split-screen targets when entering + * recents (for example: the dividerBar). + * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled). + * @param appTargets apps that will be re-parented to display area + */ + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + in RemoteAnimationTarget[] appTargets) = 12; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java index d0998eb57633..a47a15287dda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java @@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import android.annotation.Nullable; import android.graphics.Rect; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -38,33 +39,34 @@ class MainStage extends StageTaskListener { MainStage(ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession) { - super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession); + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + stageTaskUnfoldController); } boolean isActive() { return mIsActive; } - void activate(Rect rootBounds, WindowContainerTransaction wct) { + void activate(Rect rootBounds, WindowContainerTransaction wct, boolean includingTopTask) { if (mIsActive) return; final WindowContainerToken rootToken = mRootTaskInfo.token; wct.setBounds(rootToken, rootBounds) .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW) - .setLaunchRoot( - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES) - .reparentTasks( - null /* currentParent */, - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES, - true /* onTop */) // Moving the root task to top after the child tasks were re-parented , or the root // task cannot be visible and focused. .reorder(rootToken, true /* onTop */); + if (includingTopTask) { + wct.reparentTasks( + null /* currentParent */, + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + true /* onTop */, + true /* reparentTopOnly */); + } mIsActive = true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java new file mode 100644 index 000000000000..a459c8dbfa34 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.view.IWindow; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; + +import com.android.wm.shell.R; + +/** + * Handles drawing outline of the bounds of provided root surface. The outline will be drown with + * the consideration of display insets like status bar, navigation bar and display cutout. + */ +class OutlineManager extends WindowlessWindowManager { + private static final String WINDOW_NAME = "SplitOutlineLayer"; + private final Context mContext; + private final Rect mRootBounds = new Rect(); + private final Rect mTempRect = new Rect(); + private final Rect mLastOutlineBounds = new Rect(); + private final InsetsState mInsetsState = new InsetsState(); + private final int mExpandedTaskBarHeight; + private OutlineView mOutlineView; + private SurfaceControlViewHost mViewHost; + private SurfaceControl mHostLeash; + private SurfaceControl mLeash; + + OutlineManager(Context context, Configuration configuration) { + super(configuration, null /* rootSurface */, null /* hostInputToken */); + mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, + null /* options */); + mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + b.setParent(mHostLeash); + } + + void inflate(SurfaceControl rootLeash, Rect rootBounds) { + if (mLeash != null || mViewHost != null) return; + + mHostLeash = rootLeash; + mRootBounds.set(rootBounds); + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + + final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext) + .inflate(R.layout.split_outline, null); + mOutlineView = rootLayout.findViewById(R.id.split_outline); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); + lp.width = mRootBounds.width(); + lp.height = mRootBounds.height(); + lp.token = new Binder(); + lp.setTitle(WINDOW_NAME); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports + // TRUSTED_OVERLAY for windowless window without input channel. + mViewHost.setView(rootLayout, lp); + mLeash = getSurfaceControl(mViewHost.getWindowToken()); + + drawOutline(); + } + + void release() { + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + mRootBounds.setEmpty(); + mLastOutlineBounds.setEmpty(); + mOutlineView = null; + mHostLeash = null; + mLeash = null; + } + + @Nullable + SurfaceControl getOutlineLeash() { + return mLeash; + } + + void setVisibility(boolean visible) { + if (mOutlineView != null) { + mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + } + + void setRootBounds(Rect rootBounds) { + if (mViewHost == null || mViewHost.getView() == null) { + return; + } + + if (!mRootBounds.equals(rootBounds)) { + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = rootBounds.width(); + lp.height = rootBounds.height(); + mViewHost.relayout(lp); + mRootBounds.set(rootBounds); + drawOutline(); + } + } + + void onInsetsChanged(InsetsState insetsState) { + if (!mInsetsState.equals(insetsState)) { + mInsetsState.set(insetsState); + drawOutline(); + } + } + + private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) { + outBounds.set(rootBounds); + final InsetsSource taskBarInsetsSource = + insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + // Only insets the divider bar with task bar when it's expanded so that the rounded corners + // will be drawn against task bar. + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds)); + } + + // Offset the coordinate from screen based to surface based. + outBounds.offset(-rootBounds.left, -rootBounds.top); + } + + void drawOutline() { + if (mOutlineView == null) { + return; + } + + computeOutlineBounds(mRootBounds, mInsetsState, mTempRect); + if (mTempRect.equals(mLastOutlineBounds)) { + return; + } + + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams(); + lp.leftMargin = mTempRect.left; + lp.topMargin = mTempRect.top; + lp.width = mTempRect.width(); + lp.height = mTempRect.height(); + mOutlineView.setLayoutParams(lp); + mLastOutlineBounds.set(mTempRect); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java new file mode 100644 index 000000000000..94dd9b24875a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; +import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; +import static android.view.RoundedCorner.POSITION_TOP_LEFT; +import static android.view.RoundedCorner.POSITION_TOP_RIGHT; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.RoundedCorner; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.R; + +/** View for drawing split outline. */ +public class OutlineView extends View { + private final Paint mPaint = new Paint(); + private final Path mPath = new Path(); + private final float[] mRadii = new float[8]; + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth( + getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width)); + mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null)); + } + + @Override + protected void onAttachedToWindow() { + // TODO(b/200850654): match the screen corners with the actual display decor. + mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT); + mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT); + mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT); + mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT); + } + + private int getCornerRadius(@RoundedCorner.Position int position) { + final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position); + return roundedCorner == null ? 0 : roundedCorner.getRadius(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (changed) { + mPath.reset(); + mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawPath(mPath, mPaint); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java index 82f95a4f32ea..dc8fb9fbd7a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java @@ -16,27 +16,41 @@ package com.android.wm.shell.splitscreen; +import android.annotation.CallSuper; +import android.annotation.Nullable; import android.app.ActivityManager; +import android.content.Context; import android.graphics.Rect; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; /** * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up * here. All other task are launch in the {@link MainStage}. + * * @see StageCoordinator */ -class SideStage extends StageTaskListener { +class SideStage extends StageTaskListener implements + DisplayInsetsController.OnInsetsChangedListener { private static final String TAG = SideStage.class.getSimpleName(); + private final Context mContext; + private OutlineManager mOutlineManager; - SideStage(ShellTaskOrganizer taskOrganizer, int displayId, + SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession) { - super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession); + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + stageTaskUnfoldController); + mContext = context; } void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, @@ -44,7 +58,7 @@ class SideStage extends StageTaskListener { final WindowContainerToken rootToken = mRootTaskInfo.token; wct.setBounds(rootToken, rootBounds) .reparent(task.token, rootToken, true /* onTop*/) - // Moving the root task to top after the child tasks were repareted , or the root + // Moving the root task to top after the child tasks were reparented , or the root // task cannot be visible and focused. .reorder(rootToken, true /* onTop */); } @@ -69,4 +83,62 @@ class SideStage extends StageTaskListener { wct.reparent(task.token, newParent, false /* onTop */); return true; } + + @Nullable + public SurfaceControl getOutlineLeash() { + return mOutlineManager.getOutlineLeash(); + } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + super.onTaskAppeared(taskInfo, leash); + if (isRootTask(taskInfo)) { + mOutlineManager = new OutlineManager(mContext, taskInfo.configuration); + enableOutline(true); + } + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + super.onTaskInfoChanged(taskInfo); + if (isRootTask(taskInfo)) { + mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds()); + } + } + + private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) { + return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId; + } + + void enableOutline(boolean enable) { + if (mOutlineManager == null) { + return; + } + + if (enable) { + if (mRootTaskInfo != null) { + mOutlineManager.inflate(mRootLeash, + mRootTaskInfo.configuration.windowConfiguration.getBounds()); + } + } else { + mOutlineManager.release(); + } + } + + void setOutlineVisibility(boolean visible) { + mOutlineManager.setVisibility(visible); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mOutlineManager.onInsetsChanged(insetsState); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsChanged(insetsState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 002bfb6e429f..e86462f666c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -17,10 +17,13 @@ package com.android.wm.shell.splitscreen; import android.annotation.IntDef; +import android.annotation.NonNull; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import java.util.concurrent.Executor; + /** * Interface to engage split-screen feature. * TODO: Figure out which of these are actually needed outside of the Shell @@ -53,10 +56,18 @@ public interface SplitScreen { /** Callback interface for listening to changes in a split-screen stage. */ interface SplitScreenListener { - void onStagePositionChanged(@StageType int stage, @SplitPosition int position); - void onTaskStageChanged(int taskId, @StageType int stage, boolean visible); + default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {} + default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {} + default void onSplitVisibilityChanged(boolean visible) {} } + /** Registers listener that gets split screen callback. */ + void registerSplitScreenListener(@NonNull SplitScreenListener listener, + @NonNull Executor executor); + + /** Unregisters listener that gets split screen callback. */ + void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); + /** * Returns a binder that can be passed to an external process to manipulate SplitScreen. */ @@ -64,6 +75,18 @@ public interface SplitScreen { return null; } + /** + * Called when the keyguard occluded state changes. + * @param occluded Indicates if the keyguard is now occluded. + */ + void onKeyguardOccludedChanged(boolean occluded); + + /** + * Called when the visibility of the keyguard changes. + * @param showing Indicates if the keyguard is now visible. + */ + void onKeyguardVisibilityChanged(boolean showing); + /** Get a string representation of a stage type */ static String stageTypeToString(@StageType int stage) { switch (stage) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 9a457b5fd88e..ec71fbee9a29 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -16,15 +16,14 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityManager.START_SUCCESS; +import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -38,16 +37,27 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.Slog; -import android.window.IRemoteTransition; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; +import android.window.RemoteTransition; +import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.logging.InstanceId; +import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -55,16 +65,22 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.common.split.SplitLayout.SplitPosition; import com.android.wm.shell.draganddrop.DragAndDropPolicy; -import com.android.wm.shell.splitscreen.ISplitScreenListener; +import com.android.wm.shell.transition.LegacyTransitions; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.inject.Provider; /** * Class manages split-screen multitasking mode and implements the main interface * {@link SplitScreen}. * @see StageCoordinator */ +// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. public class SplitScreenController implements DragAndDropPolicy.Starter, RemoteCallable<SplitScreenController> { private static final String TAG = SplitScreenController.class.getSimpleName(); @@ -76,8 +92,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final ShellExecutor mMainExecutor; private final SplitScreenImpl mImpl = new SplitScreenImpl(); private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private final Transitions mTransitions; private final TransactionPool mTransactionPool; + private final SplitscreenEventLogger mLogger; + private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider; private StageCoordinator mStageCoordinator; @@ -85,15 +104,20 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, SyncTransactionQueue syncQueue, Context context, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellExecutor mainExecutor, DisplayImeController displayImeController, - Transitions transitions, TransactionPool transactionPool) { + DisplayInsetsController displayInsetsController, + Transitions transitions, TransactionPool transactionPool, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mTransitions = transitions; mTransactionPool = transactionPool; + mUnfoldControllerProvider = unfoldControllerProvider; + mLogger = new SplitscreenEventLogger(); } public SplitScreen asSplitScreen() { @@ -114,8 +138,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (mStageCoordinator == null) { // TODO: Multi-display mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, - mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, mTransitions, - mTransactionPool); + mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, + mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, + mUnfoldControllerProvider); } } @@ -140,8 +165,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mStageCoordinator.removeFromSideStage(taskId); } + public void setSideStageOutline(boolean enable) { + mStageCoordinator.setSideStageOutline(enable); + } + public void setSideStagePosition(@SplitPosition int sideStagePosition) { - mStageCoordinator.setSideStagePosition(sideStagePosition); + mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */); } public void setSideStageVisibility(boolean visible) { @@ -153,8 +182,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); } - public void exitSplitScreen() { - mStageCoordinator.exitSplitScreen(); + public void exitSplitScreen(int toTopTaskId, int exitReason) { + mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); + } + + public void onKeyguardOccludedChanged(boolean occluded) { + mStageCoordinator.onKeyguardOccludedChanged(occluded); + } + + public void onKeyguardVisibilityChanged(boolean showing) { + mStageCoordinator.onKeyguardVisibilityChanged(showing); } public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { @@ -175,10 +212,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public void startTask(int taskId, @SplitScreen.StageType int stage, @SplitPosition int position, @Nullable Bundle options) { - options = resolveStartStage(stage, position, options); + options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */); try { - ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + final int result = + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + if (result == START_SUCCESS || result == START_TASK_TO_FRONT) { + mStageCoordinator.evictOccludedChildren(position); + } } catch (RemoteException e) { Slog.e(TAG, "Failed to launch task", e); } @@ -187,13 +228,14 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public void startShortcut(String packageName, String shortcutId, @SplitScreen.StageType int stage, @SplitPosition int position, @Nullable Bundle options, UserHandle user) { - options = resolveStartStage(stage, position, options); + options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */); try { LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, options, user); + mStageCoordinator.evictOccludedChildren(position); } catch (ActivityNotFoundException e) { Slog.e(TAG, "Failed to launch shortcut", e); } @@ -202,64 +244,85 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public void startIntent(PendingIntent intent, Intent fillInIntent, @SplitScreen.StageType int stage, @SplitPosition int position, @Nullable Bundle options) { - options = resolveStartStage(stage, position, options); - - try { - intent.send(mContext, 0, fillInIntent, null, null, null, options); - } catch (PendingIntent.CanceledException e) { - Slog.e(TAG, "Failed to launch activity", e); + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, stage, position, options); + return; } + mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options, + null /* remote */); } - private Bundle resolveStartStage(@SplitScreen.StageType int stage, - @SplitPosition int position, @Nullable Bundle options) { - switch (stage) { - case STAGE_TYPE_UNDEFINED: { - // Use the stage of the specified position is valid. - if (position != SPLIT_POSITION_UNDEFINED) { - if (position == mStageCoordinator.getSideStagePosition()) { - options = resolveStartStage(STAGE_TYPE_SIDE, position, options); - } else { - options = resolveStartStage(STAGE_TYPE_MAIN, position, options); + private void startIntentLegacy(PendingIntent intent, Intent fillInIntent, + @SplitScreen.StageType int stage, @SplitPosition int position, + @Nullable Bundle options) { + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + mStageCoordinator.updateSurfaceBounds(null /* layout */, t); + + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } } - } else { - // Exit split-screen and launch fullscreen since stage wasn't specified. - mStageCoordinator.exitSplitScreen(); - } - break; - } - case STAGE_TYPE_SIDE: { - if (position != SPLIT_POSITION_UNDEFINED) { - mStageCoordinator.setSideStagePosition(position); - } else { - position = mStageCoordinator.getSideStagePosition(); } - if (options == null) { - options = new Bundle(); - } - mStageCoordinator.updateActivityOptions(options, position); - break; - } - case STAGE_TYPE_MAIN: { - if (position != SPLIT_POSITION_UNDEFINED) { - // Set the side stage opposite of what we want to the main stage. - final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; - mStageCoordinator.setSideStagePosition(sideStagePosition); - } else { - position = mStageCoordinator.getMainStagePosition(); - } - if (options == null) { - options = new Bundle(); + + t.apply(); + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); + } } - mStageCoordinator.updateActivityOptions(options, position); - break; + + // Launching a new app into a specific split evicts tasks previously in the same + // split. + mStageCoordinator.evictOccludedChildren(position); } - default: - throw new IllegalArgumentException("Unknown stage=" + stage); + }; + WindowContainerTransaction wct = new WindowContainerTransaction(); + options = mStageCoordinator.resolveStartStage(stage, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { + if (!isSplitScreenVisible()) return null; + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("RecentsAnimationSplitTasks") + .setHidden(false) + .setCallsite("SplitScreenController#onGoingtoRecentsLegacy"); + mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder); + SurfaceControl sc = builder.build(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + + // Ensure that we order these in the parent in the right z-order as their previous order + Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex); + int layer = 1; + for (RemoteAnimationTarget appTarget : apps) { + transaction.reparent(appTarget.leash, sc); + transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, + appTarget.screenSpaceBounds.top); + transaction.setLayer(appTarget.leash, layer++); } + transaction.apply(); + transaction.close(); + return new RemoteAnimationTarget[]{ + mStageCoordinator.getDividerBarLegacyTarget(), + mStageCoordinator.getOutlineLegacyTarget()}; + } - return options; + /** + * Sets drag info to be logged when splitscreen is entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mStageCoordinator.logOnDroppedToSplit(position, dragSessionId); } public void dump(@NonNull PrintWriter pw, String prefix) { @@ -275,6 +338,38 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @ExternalThread private class SplitScreenImpl implements SplitScreen { private ISplitScreenImpl mISplitScreen; + private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); + private final SplitScreen.SplitScreenListener mListener = new SplitScreenListener() { + @Override + public void onStagePositionChanged(int stage, int position) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onStagePositionChanged(stage, position); + }); + } + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible); + }); + } + } + + @Override + public void onSplitVisibilityChanged(boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onSplitVisibilityChanged(visible); + }); + } + } + }; @Override public ISplitScreen createExternalInterface() { @@ -284,6 +379,48 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); return mISplitScreen; } + + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardOccludedChanged(occluded); + }); + } + + @Override + public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { + if (mExecutors.containsKey(listener)) return; + + mMainExecutor.execute(() -> { + if (mExecutors.size() == 0) { + SplitScreenController.this.registerSplitScreenListener(mListener); + } + + mExecutors.put(listener, executor); + }); + + executor.execute(() -> { + mStageCoordinator.sendStatusToListener(listener); + }); + } + + @Override + public void unregisterSplitScreenListener(SplitScreenListener listener) { + mMainExecutor.execute(() -> { + mExecutors.remove(listener); + + if (mExecutors.size() == 0) { + SplitScreenController.this.unregisterSplitScreenListener(mListener); + } + }); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardVisibilityChanged(showing); + }); + } } /** @@ -377,10 +514,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void exitSplitScreen() { + public void exitSplitScreen(int toTopTaskId) { executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", (controller) -> { - controller.exitSplitScreen(); + controller.exitSplitScreen(toTopTaskId, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT); }); } @@ -417,10 +555,20 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + RemoteAnimationAdapter adapter) { + executeRemoteCallWithTaskPermission(mController, "startTasks", + (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( + mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, + adapter)); + } + + @Override public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - @Nullable IRemoteTransition remoteTransition) { + @Nullable RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mController, "startTasks", (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, remoteTransition)); @@ -444,5 +592,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, controller.startIntent(intent, fillInIntent, stage, position, options); }); } + + @Override + public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + RemoteAnimationTarget[] apps) { + final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; + executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", + (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), + true /* blocking */); + return out[0]; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index c37789ecbc9d..86e7b0e4cb7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -35,7 +35,7 @@ import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; import android.view.WindowManager; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -84,17 +84,19 @@ class SplitScreenTransitions { } void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { mFinishCallback = finishCallback; mAnimatingTransition = transition; if (mRemoteHandler != null) { - mRemoteHandler.startAnimation(transition, info, t, mRemoteFinishCB); + mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, + mRemoteFinishCB); mRemoteHandler = null; return; } - playInternalAnimation(transition, info, t, mainRoot, sideRoot); + playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); } private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -167,7 +169,7 @@ class SplitScreenTransitions { /** Starts a transition to enter split with a remote transition animator. */ IBinder startEnterTransition(@WindowManager.TransitionType int transitType, - @NonNull WindowContainerTransaction wct, @Nullable IRemoteTransition remoteTransition, + @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, @NonNull Transitions.TransitionHandler handler) { if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java new file mode 100644 index 000000000000..319079baaccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.common.split.SplitLayout.SplitPosition; + +/** + * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent + */ +public class SplitscreenEventLogger { + + // Used to generate instance ids for this drag if one is not provided + private final InstanceIdSequence mIdSequence; + + // The instance id for the current splitscreen session (from start to end) + private InstanceId mLoggerSessionId; + + // Drag info + private @SplitPosition int mDragEnterPosition; + private InstanceId mDragEnterSessionId; + + // For deduping async events + private int mLastMainStagePosition = -1; + private int mLastMainStageUid = -1; + private int mLastSideStagePosition = -1; + private int mLastSideStageUid = -1; + private float mLastSplitRatio = -1f; + + public SplitscreenEventLogger() { + mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); + } + + /** + * Return whether a splitscreen session has started. + */ + public boolean hasStartedSession() { + return mLoggerSessionId != null; + } + + /** + * May be called before logEnter() to indicate that the session was started from a drag. + */ + public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { + mDragEnterPosition = position; + mDragEnterSessionId = dragSessionId; + } + + /** + * Logs when the user enters splitscreen. + */ + public void logEnter(float splitRatio, + @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + mLoggerSessionId = mIdSequence.newInstanceId(); + int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED + ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) + : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + updateSplitRatioState(splitRatio); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER, + enterReason, + 0 /* exitReason */, + splitRatio, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mLoggerSessionId.getId()); + } + + /** + * Logs when the user exits splitscreen. Only one of the main or side stages should be + * specified to indicate which position was focused as a part of exiting (both can be unset). + */ + public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if ((mainStagePosition != SPLIT_POSITION_UNDEFINED + && sideStagePosition != SPLIT_POSITION_UNDEFINED) + || (mainStageUid != 0 && sideStageUid != 0)) { + throw new IllegalArgumentException("Only main or side stage should be set"); + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT, + 0 /* enterReason */, + exitReason, + 0f /* splitRatio */, + getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid, + getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + + // Reset states + mLoggerSessionId = null; + mDragEnterPosition = SPLIT_POSITION_UNDEFINED; + mDragEnterSessionId = null; + mLastMainStagePosition = -1; + mLastMainStageUid = -1; + mLastSideStagePosition = -1; + mLastSideStageUid = -1; + } + + /** + * Logs when an app in the main stage changes. + */ + public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, + isLandscape), mainStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + 0 /* sideStagePosition */, + 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when an app in the side stage changes. + */ + public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, + isLandscape), sideStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + 0 /* mainStagePosition */, + 0 /* mainStageUid */, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the splitscreen ratio changes. + */ + public void logResize(float splitRatio) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (splitRatio <= 0f || splitRatio >= 1f) { + // Don't bother reporting resizes that end up dismissing the split, that will be logged + // via the exit event + return; + } + if (!updateSplitRatioState(splitRatio)) { + // Ignore if there are no user perceived changes + return; + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE, + 0 /* enterReason */, + 0 /* exitReason */, + mLastSplitRatio, + 0 /* mainStagePosition */, 0 /* mainStageUid */, + 0 /* sideStagePosition */, 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the apps in splitscreen are swapped. + */ + public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + private boolean updateMainStageState(int mainStagePosition, int mainStageUid) { + boolean changed = (mLastMainStagePosition != mainStagePosition) + || (mLastMainStageUid != mainStageUid); + if (!changed) { + return false; + } + + mLastMainStagePosition = mainStagePosition; + mLastMainStageUid = mainStageUid; + return true; + } + + private boolean updateSideStageState(int sideStagePosition, int sideStageUid) { + boolean changed = (mLastSideStagePosition != sideStagePosition) + || (mLastSideStageUid != sideStageUid); + if (!changed) { + return false; + } + + mLastSideStagePosition = sideStagePosition; + mLastSideStageUid = sideStageUid; + return true; + } + + private boolean updateSplitRatioState(float splitRatio) { + boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0; + if (!changed) { + return false; + } + + mLastSplitRatio = splitRatio; + return true; + } + + public int getDragEnterReasonFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM; + } + } + + private int getMainStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM; + } + } + + private int getSideStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 0264c5a1c55a..0cff18e2ba85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -18,13 +18,19 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; - +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; @@ -35,6 +41,7 @@ import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import static com.android.wm.shell.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; import static com.android.wm.shell.transition.Transitions.isClosingType; import static com.android.wm.shell.transition.Transitions.isOpeningType; @@ -42,16 +49,28 @@ import static com.android.wm.shell.transition.Transitions.isOpeningType; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.app.WindowConfiguration; import android.content.Context; +import android.content.Intent; import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; import android.util.Log; +import android.util.Slog; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; import android.window.DisplayAreaInfo; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; @@ -59,20 +78,26 @@ import android.window.WindowContainerTransaction; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + +import javax.inject.Provider; /** * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and @@ -99,8 +124,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final MainStage mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mMainUnfoldController; private final SideStage mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mSideUnfoldController; @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -114,14 +141,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final Context mContext; private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private final SplitScreenTransitions mSplitTransitions; - private boolean mExitSplitScreenOnHide = true; + private final SplitscreenEventLogger mLogger; + private boolean mExitSplitScreenOnHide; + private boolean mKeyguardOccluded; // TODO(b/187041611): remove this flag after totally deprecated legacy split /** Whether the device is supporting legacy split or not. */ private boolean mUseLegacySplit; - @SplitScreen.StageType int mDismissTop = NO_DISMISS; + @SplitScreen.StageType private int mDismissTop = NO_DISMISS; + + /** The target stage to dismiss to when unlock after folded. */ + @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; private final Runnable mOnTransitionAnimationComplete = () -> { // If still playing, let it finish. @@ -134,29 +167,57 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDismissTop = NO_DISMISS; }; + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = + new SplitWindowManager.ParentContainerCallbacks() { + @Override + public void attachToParentSurface(SurfaceControl.Builder b) { + mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b); + } + + @Override + public void onLeashReady(SurfaceControl leash) { + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + } + }; + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, - DisplayImeController displayImeController, Transitions transitions, - TransactionPool transactionPool) { + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; mRootTDAOrganizer = rootTDAOrganizer; mTaskOrganizer = taskOrganizer; + mLogger = logger; + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + mMainStage = new MainStage( mTaskOrganizer, mDisplayId, mMainStageListener, mSyncQueue, - mSurfaceSession); + mSurfaceSession, + mMainUnfoldController); mSideStage = new SideStage( + mContext, mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue, - mSurfaceSession); + mSurfaceSession, + mSideUnfoldController); mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage); mRootTDAOrganizer.registerListener(displayId, this); + final DeviceStateManager deviceStateManager = + mContext.getSystemService(DeviceStateManager.class); + deviceStateManager.registerCallback(taskOrganizer.getExecutor(), + new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged)); mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, mOnTransitionAnimationComplete); transitions.addHandler(this); @@ -166,7 +227,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController, - SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) { + DisplayInsetsController displayInsetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, + SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -175,10 +239,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage = mainStage; mSideStage = sideStage; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mRootTDAOrganizer.registerListener(displayId, this); mSplitLayout = splitLayout; mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, mOnTransitionAnimationComplete); + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + mLogger = logger; transitions.addHandler(this); } @@ -194,10 +262,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean moveToSideStage(ActivityManager.RunningTaskInfo task, @SplitPosition int sideStagePosition) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - setSideStagePosition(sideStagePosition); - mMainStage.activate(getMainStageBounds(), wct); + setSideStagePosition(sideStagePosition, wct); + mMainStage.activate(getMainStageBounds(), wct, true /* reparent */); mSideStage.addTask(task, getSideStageBounds(), wct); - mTaskOrganizer.applyTransaction(wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t)); return true; } @@ -215,18 +284,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return result; } + void setSideStageOutline(boolean enable) { + mSideStage.enableOutline(enable); + } + /** Starts 2 tasks in one transition. */ void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - @Nullable IRemoteTransition remoteTransition) { + @Nullable RemoteTransition remoteTransition) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mainOptions = mainOptions != null ? mainOptions : new Bundle(); sideOptions = sideOptions != null ? sideOptions : new Bundle(); - setSideStagePosition(sidePosition); + setSideStagePosition(sidePosition, wct); // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - mMainStage.activate(getMainStageBounds(), wct); + mMainStage.activate(getMainStageBounds(), wct, false /* reparent */); mSideStage.setBounds(getSideStageBounds(), wct); // Make sure the launch options will put tasks in the corresponding split roots @@ -241,6 +314,144 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this); } + /** Starts 2 tasks in one legacy transition. */ + void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + RemoteAnimationAdapter adapter) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Need to add another wrapper here in shell so that we can inject the divider bar + // and also manage the process elevation via setRunningRemote + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + RemoteAnimationTarget[] augmentedNonApps = + new RemoteAnimationTarget[nonApps.length + 1]; + for (int i = 0; i < nonApps.length; ++i) { + augmentedNonApps[i] = nonApps[i]; + } + augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget(); + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( + adapter.getCallingApplication()); + adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps, + finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + + @Override + public void onAnimationCancelled() { + try { + adapter.getRunner().onAnimationCancelled(); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + }; + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( + wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); + + if (mainOptions == null) { + mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); + } else { + ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); + mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + } + + sideOptions = sideOptions != null ? sideOptions : new Bundle(); + setSideStagePosition(sidePosition, wct); + + // Build a request WCT that will launch both apps such that task 0 is on the main stage + // while task 1 is on the side stage. + mMainStage.activate(getMainStageBounds(), wct, false /* reparent */); + mSideStage.setBounds(getSideStageBounds(), wct); + + // Make sure the launch options will put tasks in the corresponding split roots + addActivityOptions(mainOptions, mMainStage); + addActivityOptions(sideOptions, mSideStage); + + // Add task launch requests + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + + // Using legacy transitions, so we can't use blast sync since it conflicts. + mTaskOrganizer.applyTransaction(wct); + } + + public void startIntent(PendingIntent intent, Intent fillInIntent, + @SplitScreen.StageType int stage, @SplitPosition int position, + @androidx.annotation.Nullable Bundle options, + @Nullable RemoteTransition remoteTransition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + options = resolveStartStage(stage, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSplitTransitions.startEnterTransition( + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this); + } + + void evictOccludedChildren(@SplitPosition int position) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + (position == mSideStagePosition ? mSideStage : mMainStage).evictOccludedChildren(wct); + mTaskOrganizer.applyTransaction(wct); + } + + Bundle resolveStartStage(@SplitScreen.StageType int stage, + @SplitPosition int position, @androidx.annotation.Nullable Bundle options, + @androidx.annotation.Nullable WindowContainerTransaction wct) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: { + // Use the stage of the specified position is valid. + if (position != SPLIT_POSITION_UNDEFINED) { + if (position == getSideStagePosition()) { + options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct); + } else { + options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct); + } + } else { + // Exit split-screen and launch fullscreen since stage wasn't specified. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + } + break; + } + case STAGE_TYPE_SIDE: { + if (position != SPLIT_POSITION_UNDEFINED) { + setSideStagePosition(position, wct); + } else { + position = getSideStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + case STAGE_TYPE_MAIN: { + if (position != SPLIT_POSITION_UNDEFINED) { + // Set the side stage opposite of what we want to the main stage. + final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + setSideStagePosition(sideStagePosition, wct); + } else { + position = getMainStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + default: + throw new IllegalArgumentException("Unknown stage=" + stage); + } + + return options; + } + @SplitLayout.SplitPosition int getSideStagePosition() { return mSideStagePosition; @@ -252,18 +463,25 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; } - void setSideStagePosition(@SplitPosition int sideStagePosition) { - setSideStagePosition(sideStagePosition, true /* updateVisibility */); + void setSideStagePosition(@SplitPosition int sideStagePosition, + @Nullable WindowContainerTransaction wct) { + setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); } - private void setSideStagePosition(@SplitPosition int sideStagePosition, - boolean updateVisibility) { + private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, + @Nullable WindowContainerTransaction wct) { if (mSideStagePosition == sideStagePosition) return; mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); - if (mSideStageListener.mVisible && updateVisibility) { - onStageVisibilityChanged(mSideStageListener); + if (mSideStageListener.mVisible && updateBounds) { + if (wct == null) { + // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. + onLayoutSizeChanged(mSplitLayout); + } else { + updateWindowBounds(mSplitLayout, wct); + updateUnfoldBounds(); + } } } @@ -275,24 +493,69 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } - void exitSplitScreen() { - exitSplitScreen(null /* childrenToTop */); + void onKeyguardOccludedChanged(boolean occluded) { + // Do not exit split directly, because it needs to wait for task info update to determine + // which task should remain on top after split dismissed. + mKeyguardOccluded = occluded; + } + + void onKeyguardVisibilityChanged(boolean showing) { + if (!showing && mMainStage.isActive() + && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { + exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, + SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED); + } } void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mExitSplitScreenOnHide = exitSplitScreenOnHide; } - private void exitSplitScreen(StageTaskListener childrenToTop) { + void exitSplitScreen(int toTopTaskId, int exitReason) { + StageTaskListener childrenToTop = null; + if (mMainStage.containsTask(toTopTaskId)) { + childrenToTop = mMainStage; + } else if (mSideStage.containsTask(toTopTaskId)) { + childrenToTop = mSideStage; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (childrenToTop != null) { + childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct); + } + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) { final WindowContainerTransaction wct = new WindowContainerTransaction(); + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void applyExitSplitScreen(StageTaskListener childrenToTop, + WindowContainerTransaction wct, int exitReason) { mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); mMainStage.deactivate(wct, childrenToTop == mMainStage); mTaskOrganizer.applyTransaction(wct); - // Reset divider position. + mSyncQueue.runInSync(t -> t + .setWindowCrop(mMainStage.mRootLeash, null) + .setWindowCrop(mSideStage.mRootLeash, null)); + // Hide divider and reset its position. + setDividerVisibility(false); mSplitLayout.resetDividerPosition(); + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (childrenToTop != null) { + logExitToStage(exitReason, childrenToTop == mMainStage); + } else { + logExit(exitReason); + } } - private void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop, + /** + * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates + * an existing WindowContainerTransaction (rather than applying immediately). This is intended + * to be used when exiting split might be bundled with other window operations. + */ + void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop, @NonNull WindowContainerTransaction wct) { mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); @@ -309,29 +572,26 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, void updateActivityOptions(Bundle opts, @SplitPosition int position) { addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage); - - if (!mMainStage.isActive()) { - // Activate the main stage in anticipation of an app launch. - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.setBounds(getSideStageBounds(), wct); - mTaskOrganizer.applyTransaction(wct); - } } void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { if (mListeners.contains(listener)) return; mListeners.add(listener); - listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); - listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); - mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); - mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + sendStatusToListener(listener); } void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { mListeners.remove(listener); } + void sendStatusToListener(SplitScreen.SplitScreenListener listener) { + listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + listener.onSplitVisibilityChanged(isSplitScreenVisible()); + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } + private void sendOnStagePositionChanged() { for (int i = mListeners.size() - 1; i >= 0; --i) { final SplitScreen.SplitScreenListener l = mListeners.get(i); @@ -340,9 +600,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - private void onStageChildTaskStatusChanged( - StageListenerImpl stageListener, int taskId, boolean present, boolean visible) { - + private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, + boolean present, boolean visible) { int stage; if (present) { stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; @@ -350,12 +609,31 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // No longer on any stage stage = STAGE_TYPE_UNDEFINED; } + if (stage == STAGE_TYPE_MAIN) { + mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } else { + mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } for (int i = mListeners.size() - 1; i >= 0; --i) { mListeners.get(i).onTaskStageChanged(taskId, stage, visible); } } + private void sendSplitVisibilityChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onSplitVisibilityChanged(mDividerVisible); + } + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); + mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); + } + } + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); @@ -392,83 +670,82 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDividerVisible = visible; if (visible) { mSplitLayout.init(); + updateUnfoldBounds(); } else { mSplitLayout.release(); } + sendSplitVisibilityChanged(); } private void onStageVisibilityChanged(StageListenerImpl stageListener) { final boolean sideStageVisible = mSideStageListener.mVisible; final boolean mainStageVisible = mMainStageListener.mVisible; - // Divider is only visible if both the main stage and side stages are visible - setDividerVisibility(isSplitScreenVisible()); - - if (mExitSplitScreenOnHide && !mainStageVisible && !sideStageVisible) { - // Exit split-screen if both stage are not visible. - // TODO: This is only a temporary request from UX and is likely to be removed soon... - exitSplitScreen(); + final boolean bothStageVisible = sideStageVisible && mainStageVisible; + final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible; + final boolean sameVisibility = sideStageVisible == mainStageVisible; + // Only add or remove divider when both visible or both invisible to avoid sometimes we only + // got one stage visibility changed for a moment and it will cause flicker. + if (sameVisibility) { + setDividerVisibility(bothStageVisible); } - if (mainStageVisible) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (sideStageVisible) { - // The main stage configuration should to follow split layout when side stage is - // visible. - mMainStage.updateConfiguration( - WINDOWING_MODE_MULTI_WINDOW, getMainStageBounds(), wct); - } else { - // We want the main stage configuration to be fullscreen when the side stage isn't - // visible. - mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct); + if (bothStageInvisible) { + if (mExitSplitScreenOnHide + // Don't dismiss staged split when both stages are not visible due to sleeping display, + // like the cases keyguard showing or screen off. + || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) { + exitSplitScreen(null /* childrenToTop */, + SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); } - // TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable. - mTaskOrganizer.applyTransaction(wct); + } else if (mKeyguardOccluded) { + // At least one of the stages is visible while keyguard occluded. Dismiss split because + // there's show-when-locked activity showing on top of keyguard. Also make sure the + // task contains show-when-locked activity remains on top after split dismissed. + final StageTaskListener toTop = + mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null); + exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP); } mSyncQueue.runInSync(t -> { - final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - final SurfaceControl sideStageLeash = mSideStage.mRootLeash; - final SurfaceControl mainStageLeash = mMainStage.mRootLeash; - - if (dividerLeash != null) { - if (mDividerVisible) { - t.show(dividerLeash) - .setLayer(dividerLeash, Integer.MAX_VALUE) - .setPosition(dividerLeash, - mSplitLayout.getDividerBounds().left, - mSplitLayout.getDividerBounds().top); - } else { - t.hide(dividerLeash); - } + // Same above, we only set root tasks and divider leash visibility when both stage + // change to visible or invisible to avoid flicker. + if (sameVisibility) { + t.setVisibility(mSideStage.mRootLeash, bothStageVisible) + .setVisibility(mMainStage.mRootLeash, bothStageVisible); + applyDividerVisibility(t); + applyOutlineVisibility(t); } + }); + } - if (sideStageVisible) { - final Rect sideStageBounds = getSideStageBounds(); - t.show(sideStageLeash) - .setPosition(sideStageLeash, - sideStageBounds.left, sideStageBounds.top) - .setWindowCrop(sideStageLeash, - sideStageBounds.width(), sideStageBounds.height()); - } else { - t.hide(sideStageLeash); - } + private void applyDividerVisibility(SurfaceControl.Transaction t) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) { + return; + } - if (mainStageVisible) { - final Rect mainStageBounds = getMainStageBounds(); - t.show(mainStageLeash); - if (sideStageVisible) { - t.setPosition(mainStageLeash, mainStageBounds.left, mainStageBounds.top) - .setWindowCrop(mainStageLeash, - mainStageBounds.width(), mainStageBounds.height()); - } else { - // Clear window crop and position if side stage isn't visible. - t.setPosition(mainStageLeash, 0, 0) - .setWindowCrop(mainStageLeash, null); - } - } else { - t.hide(mainStageLeash); - } - }); + if (mDividerVisible) { + t.show(dividerLeash) + .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top); + } else { + t.hide(dividerLeash); + } + } + + private void applyOutlineVisibility(SurfaceControl.Transaction t) { + final SurfaceControl outlineLeash = mSideStage.getOutlineLeash(); + if (outlineLeash == null) { + return; + } + + if (mDividerVisible) { + t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER); + } else { + t.hide(outlineLeash); + } } private void onStageHasChildrenChanged(StageListenerImpl stageListener) { @@ -477,22 +754,25 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!hasChildren) { if (isSideStage && mMainStageListener.mVisible) { // Exit to main stage if side stage no longer has children. - exitSplitScreen(mMainStage); + exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); } else if (!isSideStage && mSideStageListener.mVisible) { // Exit to side stage if main stage no longer has children. - exitSplitScreen(mSideStage); + exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); } } else if (isSideStage) { final WindowContainerTransaction wct = new WindowContainerTransaction(); // Make sure the main stage is active. - mMainStage.activate(getMainStageBounds(), wct); + mMainStage.activate(getMainStageBounds(), wct, true /* reparent */); mSideStage.setBounds(getSideStageBounds(), wct); - // Reorder side stage to the top whenever there's a new child task appeared in side - // stage. This is needed to prevent main stage occludes side stage and makes main stage - // flipping between fullscreen and multi-window windowing mode. - wct.reorder(mSideStage.mRootTaskInfo.token, true); mTaskOrganizer.applyTransaction(wct); } + if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren + && mSideStageListener.mHasChildren) { + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } } @VisibleForTesting @@ -511,38 +791,67 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, onSnappedToDismissTransition(mainStageToTop); return; } - exitSplitScreen(mainStageToTop ? mMainStage : mSideStage); + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, + SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER); } @Override public void onDoubleTappedDivider() { setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT); + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + + @Override + public void onLayoutPositionChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + } + + @Override + public void onLayoutSizeChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSideStage.setOutlineVisibility(false); } @Override - public void onBoundsChanging(SplitLayout layout) { + public void onLayoutSizeChanged(SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateWindowBounds(layout, wct); + updateUnfoldBounds(); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSideStage.setOutlineVisibility(true); + mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); + } + + private void updateUnfoldBounds() { + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onLayoutChanged(getMainStageBounds()); + mSideUnfoldController.onLayoutChanged(getSideStageBounds()); + } + } + + /** + * Populates `wct` with operations that match the split windows to the current layout. + * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied + */ + private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - - mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash, - bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer)); + layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); } - @Override - public void onBoundsChanged(SplitLayout layout) { + void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) { final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash, - bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer)); + (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash, + bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer); } @Override @@ -561,13 +870,30 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override + public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo, + bottomRightStage.mRootTaskInfo); + mTaskOrganizer.applyTransaction(wct); + } + + @Override public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { mDisplayAreaInfo = displayAreaInfo; if (mSplitLayout == null) { mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, - mDisplayAreaInfo.configuration, this, - b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b), + mDisplayAreaInfo.configuration, this, mParentContainerCallbacks, mDisplayImeController, mTaskOrganizer); + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.init(); + mSideUnfoldController.init(); + } } } @@ -580,8 +906,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { mDisplayAreaInfo = displayAreaInfo; if (mSplitLayout != null - && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)) { - onBoundsChanged(mSplitLayout); + && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration) + && mMainStage.isActive()) { + onLayoutSizeChanged(mSplitLayout); + } + } + + private void onFoldedStateChanged(boolean folded) { + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (!folded) return; + + if (mMainStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; } } @@ -672,7 +1010,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition != mSplitTransitions.mPendingDismiss && transition != mSplitTransitions.mPendingEnter) { @@ -717,14 +1056,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean shouldAnimate = true; if (mSplitTransitions.mPendingEnter == transition) { - shouldAnimate = startPendingEnterAnimation(transition, info, t); + shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); } else if (mSplitTransitions.mPendingDismiss == transition) { - shouldAnimate = startPendingDismissAnimation(transition, info, t); + shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction); } if (!shouldAnimate) return false; - mSplitTransitions.playAnimation(transition, info, t, finishCallback, - mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, + finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); return true; } @@ -754,7 +1093,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Update local states (before animating). setDividerVisibility(true); - setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateVisibility */); + setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */, + null /* wct */); setSplitsVisible(true); addDividerBarToTransition(info, t, true /* show */); @@ -854,12 +1194,34 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. if (show) { t.setAlpha(leash, 1.f); - t.setLayer(leash, Integer.MAX_VALUE); + t.setLayer(leash, SPLIT_DIVIDER_LAYER); t.setPosition(leash, bounds.left, bounds.top); t.show(leash); } } + RemoteAnimationTarget getDividerBarLegacyTarget() { + final Rect bounds = mSplitLayout.getDividerBounds(); + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + + RemoteAnimationTarget getOutlineLegacyTarget() { + final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds(); + // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to + // distinguish as a split auxiliary target in Launcher. + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; @@ -884,6 +1246,36 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; } + /** + * Sets drag info to be logged when splitscreen is next entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mLogger.enterRequestedByDrag(position, dragSessionId); + } + + /** + * Logs the exit of splitscreen. + */ + private void logExit(int exitReason) { + mLogger.logExit(exitReason, + SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, + SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + + /** + * Logs the exit of splitscreen to a specific stage. This must be called before the exit is + * executed. + */ + private void logExitToStage(int exitReason, boolean toMainStage) { + mLogger.logExit(exitReason, + toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED, + toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, + !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, + !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { boolean mHasRootTask = false; boolean mVisible = false; @@ -923,7 +1315,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onNoLongerSupportMultiWindow() { if (mMainStage.isActive()) { - StageCoordinator.this.exitSplitScreen(); + StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, + SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 0fd8eca6290e..071badf2bc23 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -24,6 +24,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.annotation.CallSuper; +import android.annotation.Nullable; import android.app.ActivityManager; import android.graphics.Point; import android.graphics.Rect; @@ -67,12 +68,13 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); void onRootTaskVanished(); + void onNoLongerSupportMultiWindow(); } private final StageListenerCallbacks mCallbacks; - private final SyncTransactionQueue mSyncQueue; private final SurfaceSession mSurfaceSession; + protected final SyncTransactionQueue mSyncQueue; protected ActivityManager.RunningTaskInfo mRootTaskInfo; protected SurfaceControl mRootLeash; @@ -80,12 +82,16 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>(); private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>(); + private final StageTaskUnfoldController mStageTaskUnfoldController; + StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession) { + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { mCallbacks = callbacks; mSyncQueue = syncQueue; mSurfaceSession = surfaceSession; + mStageTaskUnfoldController = stageTaskUnfoldController; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); } @@ -97,6 +103,39 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { return mChildrenTaskInfo.contains(taskId); } + /** + * Returns the top activity uid for the top child task. + */ + int getTopChildTaskUid() { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); + if (info.topActivityInfo == null) { + continue; + } + return info.topActivityInfo.applicationInfo.uid; + } + return 0; + } + + /** @return {@code true} if this listener contains the currently focused task. */ + boolean isFocused() { + if (mRootTaskInfo == null) { + return false; + } + + if (mRootTaskInfo.isFocused) { + return true; + } + + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + if (mChildrenTaskInfo.valueAt(i).isFocused) { + return true; + } + } + + return false; + } + @Override @CallSuper public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { @@ -105,8 +144,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { mRootTaskInfo = taskInfo; mCallbacks.onRootTaskAppeared(); sendStatusChanged(); - mSyncQueue.runInSync(t -> mDimLayer = - SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession)); + mSyncQueue.runInSync(t -> { + t.hide(mRootLeash); + mDimLayer = + SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession); + }); } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { final int taskId = taskInfo.taskId; mChildrenLeashes.put(taskId, leash); @@ -122,6 +164,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash); + } } @Override @@ -174,6 +220,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskVanished(taskInfo); + } } @Override @@ -191,6 +241,22 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { wct.setBounds(mRootTaskInfo.token, bounds); } + void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { + if (!containsTask(taskId)) { + return; + } + wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */); + } + + void evictOccludedChildren(WindowContainerTransaction wct) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + if (!taskInfo.isVisible) { + wct.reparent(taskInfo.token, null /* parent */, false /* onTop */); + } + } + } + void setVisibility(boolean visible, WindowContainerTransaction wct) { wct.reorder(mRootTaskInfo.token, visible /* onTop */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java new file mode 100644 index 000000000000..e904f6a9e22c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.concurrent.Executor; + +/** + * Controls transformations of the split screen task surfaces in response + * to the unfolding/folding action on foldable devices + */ +public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener { + + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; + + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final ShellUnfoldProgressProvider mUnfoldProgressProvider; + private final DisplayInsetsController mDisplayInsetsController; + private final UnfoldBackgroundController mBackgroundController; + private final Executor mExecutor; + private final int mExpandedTaskBarHeight; + private final float mWindowCornerRadiusPx; + private final Rect mStageBounds = new Rect(); + private final TransactionPool mTransactionPool; + + private InsetsSource mTaskbarInsetsSource; + private boolean mBothStagesVisible; + + public StageTaskUnfoldController(@NonNull Context context, + @NonNull TransactionPool transactionPool, + @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, + @NonNull DisplayInsetsController displayInsetsController, + @NonNull UnfoldBackgroundController backgroundController, + @NonNull Executor executor) { + mUnfoldProgressProvider = unfoldProgressProvider; + mTransactionPool = transactionPool; + mExecutor = executor; + mBackgroundController = backgroundController; + mDisplayInsetsController = displayInsetsController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + /** + * Initializes the controller, starts listening for the external events + */ + public void init() { + mUnfoldProgressProvider.addListener(mExecutor, this); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + /** + * Called when split screen task appeared + * @param taskInfo info for the appeared task + * @param leash surface leash for the appeared task + */ + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + AnimationContext context = new AnimationContext(leash); + mAnimationContextByTaskId.put(taskInfo.taskId, context); + } + + /** + * Called when a split screen task vanished + * @param taskInfo info for the vanished task + */ + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); + if (context != null) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + resetSurface(transaction, context); + transaction.apply(); + mTransactionPool.release(transaction); + } + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + @Override + public void onStateChangeProgress(float progress) { + if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return; + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + mBackgroundController.ensureBackground(transaction); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + + context.mCurrentCropRect.set(RECT_EVALUATOR + .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); + + transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + + transaction.apply(); + + mTransactionPool.release(transaction); + } + + @Override + public void onStateChangeFinished() { + resetTransformations(); + } + + /** + * Called when split screen visibility changes + * @param bothStagesVisible true if both stages of the split screen are visible + */ + public void onSplitVisibilityChanged(boolean bothStagesVisible) { + mBothStagesVisible = bothStagesVisible; + if (!bothStagesVisible) { + resetTransformations(); + } + } + + /** + * Called when split screen stage bounds changed + * @param bounds new bounds for this stage + */ + public void onLayoutChanged(Rect bounds) { + mStageBounds.set(bounds); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + private void resetTransformations() { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(transaction, context); + } + mBackgroundController.removeBackground(transaction); + transaction.apply(); + + mTransactionPool.release(transaction); + } + + private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) { + transaction + .setWindowCrop(context.mLeash, null) + .setCornerRadius(context.mLeash, 0.0F); + } + + private class AnimationContext { + final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); + final Rect mEndCropRect = new Rect(); + final Rect mCurrentCropRect = new Rect(); + + private AnimationContext(SurfaceControl leash) { + this.mLeash = leash; + update(); + } + + private void update() { + mStartCropRect.set(mStageBounds); + + if (mTaskbarInsetsSource != null) { + // Only insets the cropping window with taskbar when taskbar is expanded + if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mStartCropRect.inset(mTaskbarInsetsSource + .calculateVisibleInsets(mStartCropRect)); + } + } + + // Offset to surface coordinates as layout bounds are in screen coordinates + mStartCropRect.offsetTo(0, 0); + + mEndCropRect.set(mStartCropRect); + + int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height()); + int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION); + mStartCropRect.inset(margin, margin, margin, margin); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl new file mode 100644 index 000000000000..45f6d3c8b154 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.window.RemoteTransition; + +import com.android.wm.shell.stagesplit.ISplitScreenListener; + +/** + * Interface that is exposed to remote callers to manipulate the splitscreen feature. + */ +interface ISplitScreen { + + /** + * Registers a split screen listener. + */ + oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1; + + /** + * Unregisters a split screen listener. + */ + oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2; + + /** + * Hides the side-stage if it is currently visible. + */ + oneway void setSideStageVisibility(boolean visible) = 3; + + /** + * Removes a task from the side stage. + */ + oneway void removeFromSideStage(int taskId) = 4; + + /** + * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID + * to indicate leaving no top task after leaving split-screen. + */ + oneway void exitSplitScreen(int toTopTaskId) = 5; + + /** + * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible. + */ + oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6; + + /** + * Starts a task in a stage. + */ + oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7; + + /** + * Starts a shortcut in a stage. + */ + oneway void startShortcut(String packageName, String shortcutId, int stage, int position, + in Bundle options, in UserHandle user) = 8; + + /** + * Starts an activity in a stage. + */ + oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage, + int position, in Bundle options) = 9; + + /** + * Starts tasks simultaneously in one transition. + */ + oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, + in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10; + + /** + * Version of startTasks using legacy transition system. + */ + oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, + int sideTaskId, in Bundle sideOptions, int sidePosition, + in RemoteAnimationAdapter adapter) = 11; + + /** + * Blocking call that notifies and gets additional split-screen targets when entering + * recents (for example: the dividerBar). + * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled). + * @param appTargets apps that will be re-parented to display area + */ + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + in RemoteAnimationTarget[] appTargets) = 12; +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl index 0037059e2c51..46e4299f99fa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl @@ -14,16 +14,20 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.pip - -import android.content.ComponentName -import com.android.server.wm.traces.common.windowmanager.WindowManagerState -import com.android.server.wm.traces.parser.toWindowName +package com.android.wm.shell.stagesplit; /** - * Checks that an activity [activity] is in PIP mode + * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. */ -fun WindowManagerState.isInPipMode(activity: ComponentName): Boolean { - val windowName = activity.toWindowName() - return isInPipMode(windowName) -} +oneway interface ISplitScreenListener { + + /** + * Called when the stage position changes. + */ + void onStagePositionChanged(int stage, int position); + + /** + * Called when a task changes stages. + */ + void onTaskStageChanged(int taskId, int stage, boolean visible); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java new file mode 100644 index 000000000000..83855be91e04 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.annotation.Nullable; +import android.graphics.Rect; +import android.view.SurfaceSession; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Main stage for split-screen mode. When split-screen is active all standard activity types launch + * on the main stage, except for task that are explicitly pinned to the {@link SideStage}. + * @see StageCoordinator + */ +class MainStage extends StageTaskListener { + private static final String TAG = MainStage.class.getSimpleName(); + + private boolean mIsActive = false; + + MainStage(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + stageTaskUnfoldController); + } + + boolean isActive() { + return mIsActive; + } + + void activate(Rect rootBounds, WindowContainerTransaction wct) { + if (mIsActive) return; + + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setBounds(rootToken, rootBounds) + .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW) + .setLaunchRoot( + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES) + .reparentTasks( + null /* currentParent */, + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + true /* onTop */) + // Moving the root task to top after the child tasks were re-parented , or the root + // task cannot be visible and focused. + .reorder(rootToken, true /* onTop */); + + mIsActive = true; + } + + void deactivate(WindowContainerTransaction wct) { + deactivate(wct, false /* toTop */); + } + + void deactivate(WindowContainerTransaction wct, boolean toTop) { + if (!mIsActive) return; + mIsActive = false; + + if (mRootTaskInfo == null) return; + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setLaunchRoot( + rootToken, + null, + null) + .reparentTasks( + rootToken, + null /* newParent */, + CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + CONTROLLED_ACTIVITY_TYPES, + toTop) + // We want this re-order to the bottom regardless since we are re-parenting + // all its tasks. + .reorder(rootToken, false /* onTop */); + } + + void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, bounds) + .setWindowingMode(mRootTaskInfo.token, windowingMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java new file mode 100644 index 000000000000..8fbad52c630f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.view.IWindow; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; + +import com.android.wm.shell.R; + +/** + * Handles drawing outline of the bounds of provided root surface. The outline will be drown with + * the consideration of display insets like status bar, navigation bar and display cutout. + */ +class OutlineManager extends WindowlessWindowManager { + private static final String WINDOW_NAME = "SplitOutlineLayer"; + private final Context mContext; + private final Rect mRootBounds = new Rect(); + private final Rect mTempRect = new Rect(); + private final Rect mLastOutlineBounds = new Rect(); + private final InsetsState mInsetsState = new InsetsState(); + private final int mExpandedTaskBarHeight; + private OutlineView mOutlineView; + private SurfaceControlViewHost mViewHost; + private SurfaceControl mHostLeash; + private SurfaceControl mLeash; + + OutlineManager(Context context, Configuration configuration) { + super(configuration, null /* rootSurface */, null /* hostInputToken */); + mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, + null /* options */); + mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + b.setParent(mHostLeash); + } + + void inflate(SurfaceControl rootLeash, Rect rootBounds) { + if (mLeash != null || mViewHost != null) return; + + mHostLeash = rootLeash; + mRootBounds.set(rootBounds); + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + + final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext) + .inflate(R.layout.split_outline, null); + mOutlineView = rootLayout.findViewById(R.id.split_outline); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); + lp.width = mRootBounds.width(); + lp.height = mRootBounds.height(); + lp.token = new Binder(); + lp.setTitle(WINDOW_NAME); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports + // TRUSTED_OVERLAY for windowless window without input channel. + mViewHost.setView(rootLayout, lp); + mLeash = getSurfaceControl(mViewHost.getWindowToken()); + + drawOutline(); + } + + void release() { + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + mRootBounds.setEmpty(); + mLastOutlineBounds.setEmpty(); + mOutlineView = null; + mHostLeash = null; + mLeash = null; + } + + @Nullable + SurfaceControl getOutlineLeash() { + return mLeash; + } + + void setVisibility(boolean visible) { + if (mOutlineView != null) { + mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + } + + void setRootBounds(Rect rootBounds) { + if (mViewHost == null || mViewHost.getView() == null) { + return; + } + + if (!mRootBounds.equals(rootBounds)) { + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = rootBounds.width(); + lp.height = rootBounds.height(); + mViewHost.relayout(lp); + mRootBounds.set(rootBounds); + drawOutline(); + } + } + + void onInsetsChanged(InsetsState insetsState) { + if (!mInsetsState.equals(insetsState)) { + mInsetsState.set(insetsState); + drawOutline(); + } + } + + private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) { + outBounds.set(rootBounds); + final InsetsSource taskBarInsetsSource = + insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + // Only insets the divider bar with task bar when it's expanded so that the rounded corners + // will be drawn against task bar. + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds)); + } + + // Offset the coordinate from screen based to surface based. + outBounds.offset(-rootBounds.left, -rootBounds.top); + } + + void drawOutline() { + if (mOutlineView == null) { + return; + } + + computeOutlineBounds(mRootBounds, mInsetsState, mTempRect); + if (mTempRect.equals(mLastOutlineBounds)) { + return; + } + + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams(); + lp.leftMargin = mTempRect.left; + lp.topMargin = mTempRect.top; + lp.width = mTempRect.width(); + lp.height = mTempRect.height(); + mOutlineView.setLayoutParams(lp); + mLastOutlineBounds.set(mTempRect); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java new file mode 100644 index 000000000000..92b1381fc808 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; +import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; +import static android.view.RoundedCorner.POSITION_TOP_LEFT; +import static android.view.RoundedCorner.POSITION_TOP_RIGHT; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.RoundedCorner; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.R; + +/** View for drawing split outline. */ +public class OutlineView extends View { + private final Paint mPaint = new Paint(); + private final Path mPath = new Path(); + private final float[] mRadii = new float[8]; + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth( + getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width)); + mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null)); + } + + @Override + protected void onAttachedToWindow() { + // TODO(b/200850654): match the screen corners with the actual display decor. + mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT); + mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT); + mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT); + mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT); + } + + private int getCornerRadius(@RoundedCorner.Position int position) { + final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position); + return roundedCorner == null ? 0 : roundedCorner.getRadius(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (changed) { + mPath.reset(); + mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawPath(mPath, mPaint); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java new file mode 100644 index 000000000000..55c4f3aea19a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import android.annotation.CallSuper; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up + * here. All other task are launch in the {@link MainStage}. + * + * @see StageCoordinator + */ +class SideStage extends StageTaskListener implements + DisplayInsetsController.OnInsetsChangedListener { + private static final String TAG = SideStage.class.getSimpleName(); + private final Context mContext; + private OutlineManager mOutlineManager; + + SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + stageTaskUnfoldController); + mContext = context; + } + + void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, + WindowContainerTransaction wct) { + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setBounds(rootToken, rootBounds) + .reparent(task.token, rootToken, true /* onTop*/) + // Moving the root task to top after the child tasks were reparented , or the root + // task cannot be visible and focused. + .reorder(rootToken, true /* onTop */); + } + + boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { + // No matter if the root task is empty or not, moving the root to bottom because it no + // longer preserves visible child task. + wct.reorder(mRootTaskInfo.token, false /* onTop */); + if (mChildrenTaskInfo.size() == 0) return false; + wct.reparentTasks( + mRootTaskInfo.token, + null /* newParent */, + CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + CONTROLLED_ACTIVITY_TYPES, + toTop); + return true; + } + + boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) { + final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId); + if (task == null) return false; + wct.reparent(task.token, newParent, false /* onTop */); + return true; + } + + @Nullable + public SurfaceControl getOutlineLeash() { + return mOutlineManager.getOutlineLeash(); + } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + super.onTaskAppeared(taskInfo, leash); + if (isRootTask(taskInfo)) { + mOutlineManager = new OutlineManager(mContext, taskInfo.configuration); + enableOutline(true); + } + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + super.onTaskInfoChanged(taskInfo); + if (isRootTask(taskInfo)) { + mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds()); + } + } + + private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) { + return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId; + } + + void enableOutline(boolean enable) { + if (mOutlineManager == null) { + return; + } + + if (enable) { + if (mRootTaskInfo != null) { + mOutlineManager.inflate(mRootLeash, + mRootTaskInfo.configuration.windowConfiguration.getBounds()); + } + } else { + mOutlineManager.release(); + } + } + + void setOutlineVisibility(boolean visible) { + mOutlineManager.setVisibility(visible); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mOutlineManager.onInsetsChanged(insetsState); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsChanged(insetsState); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java new file mode 100644 index 000000000000..aec81a1ee86a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.split.SplitLayout.SplitPosition; + +import java.util.concurrent.Executor; + +/** + * Interface to engage split-screen feature. + * TODO: Figure out which of these are actually needed outside of the Shell + */ +@ExternalThread +public interface SplitScreen { + /** + * Stage type isn't specified normally meaning to use what ever the default is. + * E.g. exit split-screen and launch the app in fullscreen. + */ + int STAGE_TYPE_UNDEFINED = -1; + /** + * The main stage type. + * @see MainStage + */ + int STAGE_TYPE_MAIN = 0; + + /** + * The side stage type. + * @see SideStage + */ + int STAGE_TYPE_SIDE = 1; + + @IntDef(prefix = { "STAGE_TYPE_" }, value = { + STAGE_TYPE_UNDEFINED, + STAGE_TYPE_MAIN, + STAGE_TYPE_SIDE + }) + @interface StageType {} + + /** Callback interface for listening to changes in a split-screen stage. */ + interface SplitScreenListener { + default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {} + default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {} + default void onSplitVisibilityChanged(boolean visible) {} + } + + /** Registers listener that gets split screen callback. */ + void registerSplitScreenListener(@NonNull SplitScreenListener listener, + @NonNull Executor executor); + + /** Unregisters listener that gets split screen callback. */ + void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); + + /** + * Returns a binder that can be passed to an external process to manipulate SplitScreen. + */ + default ISplitScreen createExternalInterface() { + return null; + } + + /** + * Called when the keyguard occluded state changes. + * @param occluded Indicates if the keyguard is now occluded. + */ + void onKeyguardOccludedChanged(boolean occluded); + + /** + * Called when the visibility of the keyguard changes. + * @param showing Indicates if the keyguard is now visible. + */ + void onKeyguardVisibilityChanged(boolean showing); + + /** Get a string representation of a stage type */ + static String stageTypeToString(@StageType int stage) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: return "UNDEFINED"; + case STAGE_TYPE_MAIN: return "MAIN"; + case STAGE_TYPE_SIDE: return "SIDE"; + default: return "UNKNOWN(" + stage + ")"; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java new file mode 100644 index 000000000000..94db9cd958a3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.RemoteAnimationTarget.MODE_OPENING; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; +import android.window.RemoteTransition; +import android.window.WindowContainerTransaction; + +import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.logging.InstanceId; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import com.android.wm.shell.draganddrop.DragAndDropPolicy; +import com.android.wm.shell.transition.LegacyTransitions; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.inject.Provider; + +/** + * Class manages split-screen multitasking mode and implements the main interface + * {@link SplitScreen}. + * @see StageCoordinator + */ +// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. +public class SplitScreenController implements DragAndDropPolicy.Starter, + RemoteCallable<SplitScreenController> { + private static final String TAG = SplitScreenController.class.getSimpleName(); + + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + private final Context mContext; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private final ShellExecutor mMainExecutor; + private final SplitScreenImpl mImpl = new SplitScreenImpl(); + private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; + private final Transitions mTransitions; + private final TransactionPool mTransactionPool; + private final SplitscreenEventLogger mLogger; + private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider; + + private StageCoordinator mStageCoordinator; + + public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, Context context, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + ShellExecutor mainExecutor, DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + Transitions transitions, TransactionPool transactionPool, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mRootTDAOrganizer = rootTDAOrganizer; + mMainExecutor = mainExecutor; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mTransitions = transitions; + mTransactionPool = transactionPool; + mUnfoldControllerProvider = unfoldControllerProvider; + mLogger = new SplitscreenEventLogger(); + } + + public SplitScreen asSplitScreen() { + return mImpl; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + public void onOrganizerRegistered() { + if (mStageCoordinator == null) { + // TODO: Multi-display + mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, + mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, + mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, + mUnfoldControllerProvider); + } + } + + public boolean isSplitScreenVisible() { + return mStageCoordinator.isSplitScreenVisible(); + } + + public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) { + final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); + if (task == null) { + throw new IllegalArgumentException("Unknown taskId" + taskId); + } + return moveToSideStage(task, sideStagePosition); + } + + public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @SplitPosition int sideStagePosition) { + return mStageCoordinator.moveToSideStage(task, sideStagePosition); + } + + public boolean removeFromSideStage(int taskId) { + return mStageCoordinator.removeFromSideStage(taskId); + } + + public void setSideStageOutline(boolean enable) { + mStageCoordinator.setSideStageOutline(enable); + } + + public void setSideStagePosition(@SplitPosition int sideStagePosition) { + mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */); + } + + public void setSideStageVisibility(boolean visible) { + mStageCoordinator.setSideStageVisibility(visible); + } + + public void enterSplitScreen(int taskId, boolean leftOrTop) { + moveToSideStage(taskId, + leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); + } + + public void exitSplitScreen(int toTopTaskId, int exitReason) { + mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); + } + + public void onKeyguardOccludedChanged(boolean occluded) { + mStageCoordinator.onKeyguardOccludedChanged(occluded); + } + + public void onKeyguardVisibilityChanged(boolean showing) { + mStageCoordinator.onKeyguardVisibilityChanged(showing); + } + + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); + } + + public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); + } + + public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mStageCoordinator.registerSplitScreenListener(listener); + } + + public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mStageCoordinator.unregisterSplitScreenListener(listener); + } + + public void startTask(int taskId, @SplitScreen.StageType int stage, + @SplitPosition int position, @Nullable Bundle options) { + options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */); + + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + public void startShortcut(String packageName, String shortcutId, + @SplitScreen.StageType int stage, @SplitPosition int position, + @Nullable Bundle options, UserHandle user) { + options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */); + + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + options, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + public void startIntent(PendingIntent intent, Intent fillInIntent, + @SplitScreen.StageType int stage, @SplitPosition int position, + @Nullable Bundle options) { + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, stage, position, options); + return; + } + mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options, + null /* remote */); + } + + private void startIntentLegacy(PendingIntent intent, Intent fillInIntent, + @SplitScreen.StageType int stage, @SplitPosition int position, + @Nullable Bundle options) { + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + mStageCoordinator.updateSurfaceBounds(null /* layout */, t); + + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + } + + t.apply(); + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); + } + } + } + }; + WindowContainerTransaction wct = new WindowContainerTransaction(); + options = mStageCoordinator.resolveStartStage(stage, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { + if (!isSplitScreenVisible()) return null; + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("RecentsAnimationSplitTasks") + .setHidden(false) + .setCallsite("SplitScreenController#onGoingtoRecentsLegacy"); + mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder); + SurfaceControl sc = builder.build(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + + // Ensure that we order these in the parent in the right z-order as their previous order + Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex); + int layer = 1; + for (RemoteAnimationTarget appTarget : apps) { + transaction.reparent(appTarget.leash, sc); + transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, + appTarget.screenSpaceBounds.top); + transaction.setLayer(appTarget.leash, layer++); + } + transaction.apply(); + transaction.close(); + return new RemoteAnimationTarget[]{ + mStageCoordinator.getDividerBarLegacyTarget(), + mStageCoordinator.getOutlineLegacyTarget()}; + } + + /** + * Sets drag info to be logged when splitscreen is entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mStageCoordinator.logOnDroppedToSplit(position, dragSessionId); + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + TAG); + if (mStageCoordinator != null) { + mStageCoordinator.dump(pw, prefix); + } + } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + @ExternalThread + private class SplitScreenImpl implements SplitScreen { + private ISplitScreenImpl mISplitScreen; + private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); + private final SplitScreenListener mListener = new SplitScreenListener() { + @Override + public void onStagePositionChanged(int stage, int position) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onStagePositionChanged(stage, position); + }); + } + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible); + }); + } + } + + @Override + public void onSplitVisibilityChanged(boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onSplitVisibilityChanged(visible); + }); + } + } + }; + + @Override + public ISplitScreen createExternalInterface() { + if (mISplitScreen != null) { + mISplitScreen.invalidate(); + } + mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); + return mISplitScreen; + } + + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardOccludedChanged(occluded); + }); + } + + @Override + public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { + if (mExecutors.containsKey(listener)) return; + + mMainExecutor.execute(() -> { + if (mExecutors.size() == 0) { + SplitScreenController.this.registerSplitScreenListener(mListener); + } + + mExecutors.put(listener, executor); + }); + + executor.execute(() -> { + mStageCoordinator.sendStatusToListener(listener); + }); + } + + @Override + public void unregisterSplitScreenListener(SplitScreenListener listener) { + mMainExecutor.execute(() -> { + mExecutors.remove(listener); + + if (mExecutors.size() == 0) { + SplitScreenController.this.unregisterSplitScreenListener(mListener); + } + }); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardVisibilityChanged(showing); + }); + } + } + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class ISplitScreenImpl extends ISplitScreen.Stub { + private SplitScreenController mController; + private ISplitScreenListener mListener; + private final SplitScreen.SplitScreenListener mSplitScreenListener = + new SplitScreen.SplitScreenListener() { + @Override + public void onStagePositionChanged(int stage, int position) { + try { + if (mListener != null) { + mListener.onStagePositionChanged(stage, position); + } + } catch (RemoteException e) { + Slog.e(TAG, "onStagePositionChanged", e); + } + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + try { + if (mListener != null) { + mListener.onTaskStageChanged(taskId, stage, visible); + } + } catch (RemoteException e) { + Slog.e(TAG, "onTaskStageChanged", e); + } + } + }; + private final IBinder.DeathRecipient mListenerDeathRecipient = + new IBinder.DeathRecipient() { + @Override + @BinderThread + public void binderDied() { + final SplitScreenController controller = mController; + controller.getRemoteCallExecutor().execute(() -> { + mListener = null; + controller.unregisterSplitScreenListener(mSplitScreenListener); + }); + } + }; + + public ISplitScreenImpl(SplitScreenController controller) { + mController = controller; + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + void invalidate() { + mController = null; + } + + @Override + public void registerSplitScreenListener(ISplitScreenListener listener) { + executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener", + (controller) -> { + if (mListener != null) { + mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } + if (listener != null) { + try { + listener.asBinder().linkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death"); + return; + } + } + mListener = listener; + controller.registerSplitScreenListener(mSplitScreenListener); + }); + } + + @Override + public void unregisterSplitScreenListener(ISplitScreenListener listener) { + executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener", + (controller) -> { + if (mListener != null) { + mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } + mListener = null; + controller.unregisterSplitScreenListener(mSplitScreenListener); + }); + } + + @Override + public void exitSplitScreen(int toTopTaskId) { + executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", + (controller) -> { + controller.exitSplitScreen(toTopTaskId, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT); + }); + } + + @Override + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide", + (controller) -> { + controller.exitSplitScreenOnHide(exitSplitScreenOnHide); + }); + } + + @Override + public void setSideStageVisibility(boolean visible) { + executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility", + (controller) -> { + controller.setSideStageVisibility(visible); + }); + } + + @Override + public void removeFromSideStage(int taskId) { + executeRemoteCallWithTaskPermission(mController, "removeFromSideStage", + (controller) -> { + controller.removeFromSideStage(taskId); + }); + } + + @Override + public void startTask(int taskId, int stage, int position, @Nullable Bundle options) { + executeRemoteCallWithTaskPermission(mController, "startTask", + (controller) -> { + controller.startTask(taskId, stage, position, options); + }); + } + + @Override + public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + RemoteAnimationAdapter adapter) { + executeRemoteCallWithTaskPermission(mController, "startTasks", + (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( + mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, + adapter)); + } + + @Override + public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, + @SplitPosition int sidePosition, + @Nullable RemoteTransition remoteTransition) { + executeRemoteCallWithTaskPermission(mController, "startTasks", + (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, + sideTaskId, sideOptions, sidePosition, remoteTransition)); + } + + @Override + public void startShortcut(String packageName, String shortcutId, int stage, int position, + @Nullable Bundle options, UserHandle user) { + executeRemoteCallWithTaskPermission(mController, "startShortcut", + (controller) -> { + controller.startShortcut(packageName, shortcutId, stage, position, + options, user); + }); + } + + @Override + public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position, + @Nullable Bundle options) { + executeRemoteCallWithTaskPermission(mController, "startIntent", + (controller) -> { + controller.startIntent(intent, fillInIntent, stage, position, options); + }); + } + + @Override + public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + RemoteAnimationTarget[] apps) { + final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; + executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", + (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), + true /* blocking */); + return out[0]; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java new file mode 100644 index 000000000000..af9a5aa501e8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; +import static com.android.wm.shell.transition.Transitions.isOpeningType; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.RemoteTransition; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.transition.OneShotRemoteHandler; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; + +/** Manages transition animations for split-screen. */ +class SplitScreenTransitions { + private static final String TAG = "SplitScreenTransitions"; + + /** Flag applied to a transition change to identify it as a divider bar for animation. */ + public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; + + private final TransactionPool mTransactionPool; + private final Transitions mTransitions; + private final Runnable mOnFinish; + + IBinder mPendingDismiss = null; + IBinder mPendingEnter = null; + + private IBinder mAnimatingTransition = null; + private OneShotRemoteHandler mRemoteHandler = null; + + private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> { + if (wct != null || wctCB != null) { + throw new UnsupportedOperationException("finish transactions not supported yet."); + } + onFinish(); + }; + + /** Keeps track of currently running animations */ + private final ArrayList<Animator> mAnimations = new ArrayList<>(); + + private Transitions.TransitionFinishCallback mFinishCallback = null; + private SurfaceControl.Transaction mFinishTransaction; + + SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, + @NonNull Runnable onFinishCallback) { + mTransactionPool = pool; + mTransitions = transitions; + mOnFinish = onFinishCallback; + } + + void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { + mFinishCallback = finishCallback; + mAnimatingTransition = transition; + if (mRemoteHandler != null) { + mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, + mRemoteFinishCB); + mRemoteHandler = null; + return; + } + playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); + } + + private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, + @NonNull WindowContainerToken sideRoot) { + mFinishTransaction = mTransactionPool.acquire(); + + // Play some place-holder fade animations + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = change.getLeash(); + final int mode = info.getChanges().get(i).getMode(); + + if (mode == TRANSIT_CHANGE) { + if (change.getParent() != null) { + // This is probably reparented, so we want the parent to be immediately visible + final TransitionInfo.Change parentChange = info.getChange(change.getParent()); + t.show(parentChange.getLeash()); + t.setAlpha(parentChange.getLeash(), 1.f); + // and then animate this layer outside the parent (since, for example, this is + // the home task animating from fullscreen to part-screen). + t.reparent(leash, info.getRootLeash()); + t.setLayer(leash, info.getChanges().size() - i); + // build the finish reparent/reposition + mFinishTransaction.reparent(leash, parentChange.getLeash()); + mFinishTransaction.setPosition(leash, + change.getEndRelOffset().x, change.getEndRelOffset().y); + } + // TODO(shell-transitions): screenshot here + final Rect startBounds = new Rect(change.getStartAbsBounds()); + if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { + // Dismissing split via snap which means the still-visible task has been + // dragged to its end position at animation start so reflect that here. + startBounds.offsetTo(change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + } + final Rect endBounds = new Rect(change.getEndAbsBounds()); + startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + startExampleResizeAnimation(leash, startBounds, endBounds); + } + if (change.getParent() != null) { + continue; + } + + if (transition == mPendingEnter && (mainRoot.equals(change.getContainer()) + || sideRoot.equals(change.getContainer()))) { + t.setWindowCrop(leash, change.getStartAbsBounds().width(), + change.getStartAbsBounds().height()); + } + boolean isOpening = isOpeningType(info.getType()); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + // fade in + startExampleAnimation(leash, true /* show */); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { + // Dismissing via snap-to-top/bottom means that the dismissed task is already + // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 + // and don't animate it so it doesn't pop-in when reparented. + t.setAlpha(leash, 0.f); + } else { + startExampleAnimation(leash, false /* show */); + } + } + } + t.apply(); + onFinish(); + } + + /** Starts a transition to enter split with a remote transition animator. */ + IBinder startEnterTransition(@WindowManager.TransitionType int transitType, + @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, + @NonNull Transitions.TransitionHandler handler) { + if (remoteTransition != null) { + // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) + mRemoteHandler = new OneShotRemoteHandler( + mTransitions.getMainExecutor(), remoteTransition); + } + final IBinder transition = mTransitions.startTransition(transitType, wct, handler); + mPendingEnter = transition; + if (mRemoteHandler != null) { + mRemoteHandler.setTransition(transition); + } + return transition; + } + + /** Starts a transition for dismissing split after dragging the divider to a screen edge */ + IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct, + @NonNull Transitions.TransitionHandler handler) { + final IBinder transition = mTransitions.startTransition( + TRANSIT_SPLIT_DISMISS_SNAP, wct, handler); + mPendingDismiss = transition; + return transition; + } + + void onFinish() { + if (!mAnimations.isEmpty()) return; + mOnFinish.run(); + if (mFinishTransaction != null) { + mFinishTransaction.apply(); + mTransactionPool.release(mFinishTransaction); + mFinishTransaction = null; + } + mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + mFinishCallback = null; + if (mAnimatingTransition == mPendingEnter) { + mPendingEnter = null; + } + if (mAnimatingTransition == mPendingDismiss) { + mPendingDismiss = null; + } + mAnimatingTransition = null; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { + final float end = show ? 1.f : 0.f; + final float start = 1.f - end; + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(start, end); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } + + // TODO(shell-transitions): real animations + private void startExampleResizeAnimation(@NonNull SurfaceControl leash, + @NonNull Rect startBounds, @NonNull Rect endBounds) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setWindowCrop(leash, + (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), + (int) (startBounds.height() * (1.f - fraction) + + endBounds.height() * fraction)); + transaction.setPosition(leash, + startBounds.left * (1.f - fraction) + endBounds.left * fraction, + startBounds.top * (1.f - fraction) + endBounds.top * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setWindowCrop(leash, 0, 0); + transaction.setPosition(leash, endBounds.left, endBounds.top); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java new file mode 100644 index 000000000000..aab7902232bf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.common.split.SplitLayout.SplitPosition; + +/** + * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent + */ +public class SplitscreenEventLogger { + + // Used to generate instance ids for this drag if one is not provided + private final InstanceIdSequence mIdSequence; + + // The instance id for the current splitscreen session (from start to end) + private InstanceId mLoggerSessionId; + + // Drag info + private @SplitPosition int mDragEnterPosition; + private InstanceId mDragEnterSessionId; + + // For deduping async events + private int mLastMainStagePosition = -1; + private int mLastMainStageUid = -1; + private int mLastSideStagePosition = -1; + private int mLastSideStageUid = -1; + private float mLastSplitRatio = -1f; + + public SplitscreenEventLogger() { + mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); + } + + /** + * Return whether a splitscreen session has started. + */ + public boolean hasStartedSession() { + return mLoggerSessionId != null; + } + + /** + * May be called before logEnter() to indicate that the session was started from a drag. + */ + public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { + mDragEnterPosition = position; + mDragEnterSessionId = dragSessionId; + } + + /** + * Logs when the user enters splitscreen. + */ + public void logEnter(float splitRatio, + @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + mLoggerSessionId = mIdSequence.newInstanceId(); + int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED + ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) + : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + updateSplitRatioState(splitRatio); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER, + enterReason, + 0 /* exitReason */, + splitRatio, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mLoggerSessionId.getId()); + } + + /** + * Logs when the user exits splitscreen. Only one of the main or side stages should be + * specified to indicate which position was focused as a part of exiting (both can be unset). + */ + public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if ((mainStagePosition != SPLIT_POSITION_UNDEFINED + && sideStagePosition != SPLIT_POSITION_UNDEFINED) + || (mainStageUid != 0 && sideStageUid != 0)) { + throw new IllegalArgumentException("Only main or side stage should be set"); + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT, + 0 /* enterReason */, + exitReason, + 0f /* splitRatio */, + getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid, + getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + + // Reset states + mLoggerSessionId = null; + mDragEnterPosition = SPLIT_POSITION_UNDEFINED; + mDragEnterSessionId = null; + mLastMainStagePosition = -1; + mLastMainStageUid = -1; + mLastSideStagePosition = -1; + mLastSideStageUid = -1; + } + + /** + * Logs when an app in the main stage changes. + */ + public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, + isLandscape), mainStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + 0 /* sideStagePosition */, + 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when an app in the side stage changes. + */ + public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, + isLandscape), sideStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + 0 /* mainStagePosition */, + 0 /* mainStageUid */, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the splitscreen ratio changes. + */ + public void logResize(float splitRatio) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (splitRatio <= 0f || splitRatio >= 1f) { + // Don't bother reporting resizes that end up dismissing the split, that will be logged + // via the exit event + return; + } + if (!updateSplitRatioState(splitRatio)) { + // Ignore if there are no user perceived changes + return; + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE, + 0 /* enterReason */, + 0 /* exitReason */, + mLastSplitRatio, + 0 /* mainStagePosition */, 0 /* mainStageUid */, + 0 /* sideStagePosition */, 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the apps in splitscreen are swapped. + */ + public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + private boolean updateMainStageState(int mainStagePosition, int mainStageUid) { + boolean changed = (mLastMainStagePosition != mainStagePosition) + || (mLastMainStageUid != mainStageUid); + if (!changed) { + return false; + } + + mLastMainStagePosition = mainStagePosition; + mLastMainStageUid = mainStageUid; + return true; + } + + private boolean updateSideStageState(int sideStagePosition, int sideStageUid) { + boolean changed = (mLastSideStagePosition != sideStagePosition) + || (mLastSideStageUid != sideStageUid); + if (!changed) { + return false; + } + + mLastSideStagePosition = sideStagePosition; + mLastSideStageUid = sideStageUid; + return true; + } + + private boolean updateSplitRatioState(float splitRatio) { + boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0; + if (!changed) { + return false; + } + + mLastSplitRatio = splitRatio; + return true; + } + + public int getDragEnterReasonFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM; + } + } + + private int getMainStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM; + } + } + + private int getSideStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java new file mode 100644 index 000000000000..574e379921b1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java @@ -0,0 +1,1330 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.transitTypeToString; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.stagesplit.SplitScreen.stageTypeToString; +import static com.android.wm.shell.stagesplit.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; +import static com.android.wm.shell.transition.Transitions.isClosingType; +import static com.android.wm.shell.transition.Transitions.isOpeningType; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; +import android.window.DisplayAreaInfo; +import android.window.RemoteTransition; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.InstanceId; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import com.android.wm.shell.common.split.SplitWindowManager; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.inject.Provider; + +/** + * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and + * {@link SideStage} stages. + * Some high-level rules: + * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at + * least one child task. + * - The {@link MainStage} should only have children if the coordinator is active. + * - The {@link SplitLayout} divider is only visible if both the {@link MainStage} + * and {@link SideStage} are visible. + * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible. + * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and + * {@link #onStageHasChildrenChanged(StageListenerImpl).} + */ +class StageCoordinator implements SplitLayout.SplitLayoutHandler, + RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler { + + private static final String TAG = StageCoordinator.class.getSimpleName(); + + /** internal value for mDismissTop that represents no dismiss */ + private static final int NO_DISMISS = -2; + + private final SurfaceSession mSurfaceSession = new SurfaceSession(); + + private final MainStage mMainStage; + private final StageListenerImpl mMainStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mMainUnfoldController; + private final SideStage mSideStage; + private final StageListenerImpl mSideStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mSideUnfoldController; + @SplitPosition + private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; + + private final int mDisplayId; + private SplitLayout mSplitLayout; + private boolean mDividerVisible; + private final SyncTransactionQueue mSyncQueue; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private final ShellTaskOrganizer mTaskOrganizer; + private DisplayAreaInfo mDisplayAreaInfo; + private final Context mContext; + private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); + private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; + private final SplitScreenTransitions mSplitTransitions; + private final SplitscreenEventLogger mLogger; + private boolean mExitSplitScreenOnHide; + private boolean mKeyguardOccluded; + + // TODO(b/187041611): remove this flag after totally deprecated legacy split + /** Whether the device is supporting legacy split or not. */ + private boolean mUseLegacySplit; + + @SplitScreen.StageType private int mDismissTop = NO_DISMISS; + + /** The target stage to dismiss to when unlock after folded. */ + @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + + private final Runnable mOnTransitionAnimationComplete = () -> { + // If still playing, let it finish. + if (!isSplitScreenVisible()) { + // Update divider state after animation so that it is still around and positioned + // properly for the animation itself. + setDividerVisibility(false); + mSplitLayout.resetDividerPosition(); + } + mDismissTop = NO_DISMISS; + }; + + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = + new SplitWindowManager.ParentContainerCallbacks() { + @Override + public void attachToParentSurface(SurfaceControl.Builder b) { + mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b); + } + + @Override + public void onLeashReady(SurfaceControl leash) { + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + } + }; + + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + mContext = context; + mDisplayId = displayId; + mSyncQueue = syncQueue; + mRootTDAOrganizer = rootTDAOrganizer; + mTaskOrganizer = taskOrganizer; + mLogger = logger; + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + + mMainStage = new MainStage( + mTaskOrganizer, + mDisplayId, + mMainStageListener, + mSyncQueue, + mSurfaceSession, + mMainUnfoldController); + mSideStage = new SideStage( + mContext, + mTaskOrganizer, + mDisplayId, + mSideStageListener, + mSyncQueue, + mSurfaceSession, + mSideUnfoldController); + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage); + mRootTDAOrganizer.registerListener(displayId, this); + final DeviceStateManager deviceStateManager = + mContext.getSystemService(DeviceStateManager.class); + deviceStateManager.registerCallback(taskOrganizer.getExecutor(), + new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged)); + mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, + mOnTransitionAnimationComplete); + transitions.addHandler(this); + } + + @VisibleForTesting + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, + SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + mContext = context; + mDisplayId = displayId; + mSyncQueue = syncQueue; + mRootTDAOrganizer = rootTDAOrganizer; + mTaskOrganizer = taskOrganizer; + mMainStage = mainStage; + mSideStage = sideStage; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mRootTDAOrganizer.registerListener(displayId, this); + mSplitLayout = splitLayout; + mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, + mOnTransitionAnimationComplete); + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + mLogger = logger; + transitions.addHandler(this); + } + + @VisibleForTesting + SplitScreenTransitions getSplitTransitions() { + return mSplitTransitions; + } + + boolean isSplitScreenVisible() { + return mSideStageListener.mVisible && mMainStageListener.mVisible; + } + + boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @SplitPosition int sideStagePosition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(sideStagePosition, wct); + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.addTask(task, getSideStageBounds(), wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t)); + return true; + } + + boolean removeFromSideStage(int taskId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + /** + * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the + * {@link SideStage} no longer has children. + */ + final boolean result = mSideStage.removeTask(taskId, + mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null, + wct); + mTaskOrganizer.applyTransaction(wct); + return result; + } + + void setSideStageOutline(boolean enable) { + mSideStage.enableOutline(enable); + } + + /** Starts 2 tasks in one transition. */ + void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, + @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + @Nullable RemoteTransition remoteTransition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mainOptions = mainOptions != null ? mainOptions : new Bundle(); + sideOptions = sideOptions != null ? sideOptions : new Bundle(); + setSideStagePosition(sidePosition, wct); + + // Build a request WCT that will launch both apps such that task 0 is on the main stage + // while task 1 is on the side stage. + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + + // Make sure the launch options will put tasks in the corresponding split roots + addActivityOptions(mainOptions, mMainStage); + addActivityOptions(sideOptions, mSideStage); + + // Add task launch requests + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + + mSplitTransitions.startEnterTransition( + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this); + } + + /** Starts 2 tasks in one legacy transition. */ + void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + RemoteAnimationAdapter adapter) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Need to add another wrapper here in shell so that we can inject the divider bar + // and also manage the process elevation via setRunningRemote + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + RemoteAnimationTarget[] augmentedNonApps = + new RemoteAnimationTarget[nonApps.length + 1]; + for (int i = 0; i < nonApps.length; ++i) { + augmentedNonApps[i] = nonApps[i]; + } + augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget(); + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( + adapter.getCallingApplication()); + adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps, + finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + + @Override + public void onAnimationCancelled() { + try { + adapter.getRunner().onAnimationCancelled(); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + }; + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( + wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); + + if (mainOptions == null) { + mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); + } else { + ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); + mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + } + + sideOptions = sideOptions != null ? sideOptions : new Bundle(); + setSideStagePosition(sidePosition, wct); + + // Build a request WCT that will launch both apps such that task 0 is on the main stage + // while task 1 is on the side stage. + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + + // Make sure the launch options will put tasks in the corresponding split roots + addActivityOptions(mainOptions, mMainStage); + addActivityOptions(sideOptions, mSideStage); + + // Add task launch requests + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + + // Using legacy transitions, so we can't use blast sync since it conflicts. + mTaskOrganizer.applyTransaction(wct); + } + + public void startIntent(PendingIntent intent, Intent fillInIntent, + @SplitScreen.StageType int stage, @SplitPosition int position, + @androidx.annotation.Nullable Bundle options, + @Nullable RemoteTransition remoteTransition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + options = resolveStartStage(stage, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSplitTransitions.startEnterTransition( + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this); + } + + Bundle resolveStartStage(@SplitScreen.StageType int stage, + @SplitPosition int position, @androidx.annotation.Nullable Bundle options, + @androidx.annotation.Nullable WindowContainerTransaction wct) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: { + // Use the stage of the specified position is valid. + if (position != SPLIT_POSITION_UNDEFINED) { + if (position == getSideStagePosition()) { + options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct); + } else { + options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct); + } + } else { + // Exit split-screen and launch fullscreen since stage wasn't specified. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + } + break; + } + case STAGE_TYPE_SIDE: { + if (position != SPLIT_POSITION_UNDEFINED) { + setSideStagePosition(position, wct); + } else { + position = getSideStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + case STAGE_TYPE_MAIN: { + if (position != SPLIT_POSITION_UNDEFINED) { + // Set the side stage opposite of what we want to the main stage. + final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + setSideStagePosition(sideStagePosition, wct); + } else { + position = getMainStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + default: + throw new IllegalArgumentException("Unknown stage=" + stage); + } + + return options; + } + + @SplitPosition + int getSideStagePosition() { + return mSideStagePosition; + } + + @SplitPosition + int getMainStagePosition() { + return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + } + + void setSideStagePosition(@SplitPosition int sideStagePosition, + @Nullable WindowContainerTransaction wct) { + setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); + } + + private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, + @Nullable WindowContainerTransaction wct) { + if (mSideStagePosition == sideStagePosition) return; + mSideStagePosition = sideStagePosition; + sendOnStagePositionChanged(); + + if (mSideStageListener.mVisible && updateBounds) { + if (wct == null) { + // onLayoutSizeChanged builds/applies a wct with the contents of updateWindowBounds. + onLayoutSizeChanged(mSplitLayout); + } else { + updateWindowBounds(mSplitLayout, wct); + updateUnfoldBounds(); + } + } + } + + void setSideStageVisibility(boolean visible) { + if (mSideStageListener.mVisible == visible) return; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSideStage.setVisibility(visible, wct); + mTaskOrganizer.applyTransaction(wct); + } + + void onKeyguardOccludedChanged(boolean occluded) { + // Do not exit split directly, because it needs to wait for task info update to determine + // which task should remain on top after split dismissed. + mKeyguardOccluded = occluded; + } + + void onKeyguardVisibilityChanged(boolean showing) { + if (!showing && mMainStage.isActive() + && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { + exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, + SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED); + } + } + + void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mExitSplitScreenOnHide = exitSplitScreenOnHide; + } + + void exitSplitScreen(int toTopTaskId, int exitReason) { + StageTaskListener childrenToTop = null; + if (mMainStage.containsTask(toTopTaskId)) { + childrenToTop = mMainStage; + } else if (mSideStage.containsTask(toTopTaskId)) { + childrenToTop = mSideStage; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (childrenToTop != null) { + childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct); + } + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void applyExitSplitScreen( + StageTaskListener childrenToTop, + WindowContainerTransaction wct, int exitReason) { + mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); + mMainStage.deactivate(wct, childrenToTop == mMainStage); + mTaskOrganizer.applyTransaction(wct); + mSyncQueue.runInSync(t -> t + .setWindowCrop(mMainStage.mRootLeash, null) + .setWindowCrop(mSideStage.mRootLeash, null)); + // Hide divider and reset its position. + setDividerVisibility(false); + mSplitLayout.resetDividerPosition(); + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (childrenToTop != null) { + logExitToStage(exitReason, childrenToTop == mMainStage); + } else { + logExit(exitReason); + } + } + + /** + * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates + * an existing WindowContainerTransaction (rather than applying immediately). This is intended + * to be used when exiting split might be bundled with other window operations. + */ + void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop, + @NonNull WindowContainerTransaction wct) { + mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); + mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); + } + + void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + outTopOrLeftBounds.set(mSplitLayout.getBounds1()); + outBottomOrRightBounds.set(mSplitLayout.getBounds2()); + } + + private void addActivityOptions(Bundle opts, StageTaskListener stage) { + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + } + + void updateActivityOptions(Bundle opts, @SplitPosition int position) { + addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage); + } + + void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + if (mListeners.contains(listener)) return; + mListeners.add(listener); + sendStatusToListener(listener); + } + + void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mListeners.remove(listener); + } + + void sendStatusToListener(SplitScreen.SplitScreenListener listener) { + listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + listener.onSplitVisibilityChanged(isSplitScreenVisible()); + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } + + private void sendOnStagePositionChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + } + } + + private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, + boolean present, boolean visible) { + int stage; + if (present) { + stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } else { + // No longer on any stage + stage = STAGE_TYPE_UNDEFINED; + } + if (stage == STAGE_TYPE_MAIN) { + mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } else { + mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onTaskStageChanged(taskId, stage, visible); + } + } + + private void sendSplitVisibilityChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onSplitVisibilityChanged(mDividerVisible); + } + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); + mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); + } + } + + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { + if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { + mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Make the stages adjacent to each other so they occlude what's behind them. + wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + + // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy + // split to prevent new split behavior confusing users. + if (!mUseLegacySplit) { + wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); + } + + mTaskOrganizer.applyTransaction(wct); + } + } + + private void onStageRootTaskVanished(StageListenerImpl stageListener) { + if (stageListener == mMainStageListener || stageListener == mSideStageListener) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Deactivate the main stage if it no longer has a root task. + mMainStage.deactivate(wct); + + if (!mUseLegacySplit) { + wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); + } + + mTaskOrganizer.applyTransaction(wct); + } + } + + private void setDividerVisibility(boolean visible) { + if (mDividerVisible == visible) return; + mDividerVisible = visible; + if (visible) { + mSplitLayout.init(); + updateUnfoldBounds(); + } else { + mSplitLayout.release(); + } + sendSplitVisibilityChanged(); + } + + private void onStageVisibilityChanged(StageListenerImpl stageListener) { + final boolean sideStageVisible = mSideStageListener.mVisible; + final boolean mainStageVisible = mMainStageListener.mVisible; + final boolean bothStageVisible = sideStageVisible && mainStageVisible; + final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible; + final boolean sameVisibility = sideStageVisible == mainStageVisible; + // Only add or remove divider when both visible or both invisible to avoid sometimes we only + // got one stage visibility changed for a moment and it will cause flicker. + if (sameVisibility) { + setDividerVisibility(bothStageVisible); + } + + if (bothStageInvisible) { + if (mExitSplitScreenOnHide + // Don't dismiss staged split when both stages are not visible due to sleeping display, + // like the cases keyguard showing or screen off. + || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) { + exitSplitScreen(null /* childrenToTop */, + SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); + } + } else if (mKeyguardOccluded) { + // At least one of the stages is visible while keyguard occluded. Dismiss split because + // there's show-when-locked activity showing on top of keyguard. Also make sure the + // task contains show-when-locked activity remains on top after split dismissed. + final StageTaskListener toTop = + mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null); + exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP); + } + + mSyncQueue.runInSync(t -> { + // Same above, we only set root tasks and divider leash visibility when both stage + // change to visible or invisible to avoid flicker. + if (sameVisibility) { + t.setVisibility(mSideStage.mRootLeash, bothStageVisible) + .setVisibility(mMainStage.mRootLeash, bothStageVisible); + applyDividerVisibility(t); + applyOutlineVisibility(t); + } + }); + } + + private void applyDividerVisibility(SurfaceControl.Transaction t) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) { + return; + } + + if (mDividerVisible) { + t.show(dividerLeash) + .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top); + } else { + t.hide(dividerLeash); + } + } + + private void applyOutlineVisibility(SurfaceControl.Transaction t) { + final SurfaceControl outlineLeash = mSideStage.getOutlineLeash(); + if (outlineLeash == null) { + return; + } + + if (mDividerVisible) { + t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER); + } else { + t.hide(outlineLeash); + } + } + + private void onStageHasChildrenChanged(StageListenerImpl stageListener) { + final boolean hasChildren = stageListener.mHasChildren; + final boolean isSideStage = stageListener == mSideStageListener; + if (!hasChildren) { + if (isSideStage && mMainStageListener.mVisible) { + // Exit to main stage if side stage no longer has children. + exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); + } else if (!isSideStage && mSideStageListener.mVisible) { + // Exit to side stage if main stage no longer has children. + exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); + } + } else if (isSideStage) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Make sure the main stage is active. + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + mTaskOrganizer.applyTransaction(wct); + } + if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren + && mSideStageListener.mHasChildren) { + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + } + + @VisibleForTesting + IBinder onSnappedToDismissTransition(boolean mainStageToTop) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct); + return mSplitTransitions.startSnapToDismiss(wct, this); + } + + @Override + public void onSnappedToDismiss(boolean bottomOrRight) { + final boolean mainStageToTop = + bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT + : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; + if (ENABLE_SHELL_TRANSITIONS) { + onSnappedToDismissTransition(mainStageToTop); + return; + } + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, + SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER); + } + + @Override + public void onDoubleTappedDivider() { + setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + + @Override + public void onLayoutPositionChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + } + + @Override + public void onLayoutSizeChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSideStage.setOutlineVisibility(false); + } + + @Override + public void onLayoutSizeChanged(SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateWindowBounds(layout, wct); + updateUnfoldBounds(); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSideStage.setOutlineVisibility(true); + mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); + } + + private void updateUnfoldBounds() { + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onLayoutChanged(getMainStageBounds()); + mSideUnfoldController.onLayoutChanged(getSideStageBounds()); + } + } + + /** + * Populates `wct` with operations that match the split windows to the current layout. + * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied + */ + private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); + } + + void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash, + bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer); + } + + @Override + public int getSplitItemPosition(WindowContainerToken token) { + if (token == null) { + return SPLIT_POSITION_UNDEFINED; + } + + if (token.equals(mMainStage.mRootTaskInfo.getToken())) { + return getMainStagePosition(); + } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) { + return getSideStagePosition(); + } + + return SPLIT_POSITION_UNDEFINED; + } + + @Override + public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo, + bottomRightStage.mRootTaskInfo); + mTaskOrganizer.applyTransaction(wct); + } + + @Override + public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { + mDisplayAreaInfo = displayAreaInfo; + if (mSplitLayout == null) { + mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, + mDisplayAreaInfo.configuration, this, mParentContainerCallbacks, + mDisplayImeController, mTaskOrganizer); + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.init(); + mSideUnfoldController.init(); + } + } + } + + @Override + public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + throw new IllegalStateException("Well that was unexpected..."); + } + + @Override + public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { + mDisplayAreaInfo = displayAreaInfo; + if (mSplitLayout != null + && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration) + && mMainStage.isActive()) { + onLayoutSizeChanged(mSplitLayout); + } + } + + private void onFoldedStateChanged(boolean folded) { + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (!folded) return; + + if (mMainStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; + } + } + + private Rect getSideStageBounds() { + return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); + } + + private Rect getMainStageBounds() { + return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); + } + + /** + * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain + * this task (yet) so this can also be used to identify which stage to put a task into. + */ + private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) { + // TODO(b/184679596): Find a way to either include task-org information in the transition, + // or synchronize task-org callbacks so we can use stage.containsTask + if (mMainStage.mRootTaskInfo != null + && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) { + return mMainStage; + } else if (mSideStage.mRootTaskInfo != null + && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) { + return mSideStage; + } + return null; + } + + @SplitScreen.StageType + private int getStageType(StageTaskListener stage) { + return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @Nullable TransitionRequestInfo request) { + final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); + if (triggerTask == null) { + // still want to monitor everything while in split-screen, so return non-null. + return isSplitScreenVisible() ? new WindowContainerTransaction() : null; + } + + WindowContainerTransaction out = null; + final @WindowManager.TransitionType int type = request.getType(); + if (isSplitScreenVisible()) { + // try to handle everything while in split-screen, so return a WCT even if it's empty. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split" + + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d" + + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type), + mMainStage.getChildCount(), mSideStage.getChildCount()); + out = new WindowContainerTransaction(); + final StageTaskListener stage = getStageOfTask(triggerTask); + if (stage != null) { + // dismiss split if the last task in one of the stages is going away + if (isClosingType(type) && stage.getChildCount() == 1) { + // The top should be the opposite side that is closing: + mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN + ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } + } else { + if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) { + // Going home so dismiss both. + mDismissTop = STAGE_TYPE_UNDEFINED; + } + } + if (mDismissTop != NO_DISMISS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Dismiss from request. toTop=%s", + stageTypeToString(mDismissTop)); + prepareExitSplitScreen(mDismissTop, out); + mSplitTransitions.mPendingDismiss = transition; + } + } else { + // Not in split mode, so look for an open into a split stage just so we can whine and + // complain about how this isn't a supported operation. + if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) { + if (getStageOfTask(triggerTask) != null) { + throw new IllegalStateException("Entering split implicitly with only one task" + + " isn't supported."); + } + } + } + return out; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (transition != mSplitTransitions.mPendingDismiss + && transition != mSplitTransitions.mPendingEnter) { + // Not entering or exiting, so just do some house-keeping and validation. + + // If we're not in split-mode, just abort so something else can handle it. + if (!isSplitScreenVisible()) return false; + + for (int iC = 0; iC < info.getChanges().size(); ++iC) { + final TransitionInfo.Change change = info.getChanges().get(iC); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || !taskInfo.hasParentTask()) continue; + final StageTaskListener stage = getStageOfTask(taskInfo); + if (stage == null) continue; + if (isOpeningType(change.getMode())) { + if (!stage.containsTask(taskInfo.taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called" + + " with " + taskInfo.taskId + " before startAnimation()."); + } + } else if (isClosingType(change.getMode())) { + if (stage.containsTask(taskInfo.taskId)) { + Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called" + + " with " + taskInfo.taskId + " before startAnimation()."); + } + } + } + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + // TODO(shell-transitions): Implement a fallback behavior for now. + throw new IllegalStateException("Somehow removed the last task in a stage" + + " outside of a proper transition"); + // This can happen in some pathological cases. For example: + // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C] + // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time + // In this case, the result *should* be that we leave split. + // TODO(b/184679596): Find a way to either include task-org information in + // the transition, or synchronize task-org callbacks. + } + + // Use normal animations. + return false; + } + + boolean shouldAnimate = true; + if (mSplitTransitions.mPendingEnter == transition) { + shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); + } else if (mSplitTransitions.mPendingDismiss == transition) { + shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction); + } + if (!shouldAnimate) return false; + + mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, + finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + return true; + } + + private boolean startPendingEnterAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) { + // First, verify that we actually have opened 2 apps in split. + TransitionInfo.Change mainChild = null; + TransitionInfo.Change sideChild = null; + for (int iC = 0; iC < info.getChanges().size(); ++iC) { + final TransitionInfo.Change change = info.getChanges().get(iC); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || !taskInfo.hasParentTask()) continue; + final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo)); + if (stageType == STAGE_TYPE_MAIN) { + mainChild = change; + } else if (stageType == STAGE_TYPE_SIDE) { + sideChild = change; + } + } + if (mainChild == null || sideChild == null) { + throw new IllegalStateException("Launched 2 tasks in split, but didn't receive" + + " 2 tasks in transition. Possibly one of them failed to launch"); + // TODO: fallback logic. Probably start a new transition to exit split before + // applying anything here. Ideally consolidate with transition-merging. + } + + // Update local states (before animating). + setDividerVisibility(true); + setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */, + null /* wct */); + setSplitsVisible(true); + + addDividerBarToTransition(info, t, true /* show */); + + // Make some noise if things aren't totally expected. These states shouldn't effect + // transitions locally, but remotes (like Launcher) may get confused if they were + // depending on listener callbacks. This can happen because task-organizer callbacks + // aren't serialized with transition callbacks. + // TODO(b/184679596): Find a way to either include task-org information in + // the transition, or synchronize task-org callbacks. + if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + mMainStage + + " to have been called with " + mainChild.getTaskInfo().taskId + + " before startAnimation()."); + } + if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + mSideStage + + " to have been called with " + sideChild.getTaskInfo().taskId + + " before startAnimation()."); + } + return true; + } else { + // TODO: other entry method animations + throw new RuntimeException("Unsupported split-entry"); + } + } + + private boolean startPendingDismissAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + // Make some noise if things aren't totally expected. These states shouldn't effect + // transitions locally, but remotes (like Launcher) may get confused if they were + // depending on listener callbacks. This can happen because task-organizer callbacks + // aren't serialized with transition callbacks. + // TODO(b/184679596): Find a way to either include task-org information in + // the transition, or synchronize task-org callbacks. + if (mMainStage.getChildCount() != 0) { + final StringBuilder tasksLeft = new StringBuilder(); + for (int i = 0; i < mMainStage.getChildCount(); ++i) { + tasksLeft.append(i != 0 ? ", " : ""); + tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i)); + } + Log.w(TAG, "Expected onTaskVanished on " + mMainStage + + " to have been called with [" + tasksLeft.toString() + + "] before startAnimation()."); + } + if (mSideStage.getChildCount() != 0) { + final StringBuilder tasksLeft = new StringBuilder(); + for (int i = 0; i < mSideStage.getChildCount(); ++i) { + tasksLeft.append(i != 0 ? ", " : ""); + tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i)); + } + Log.w(TAG, "Expected onTaskVanished on " + mSideStage + + " to have been called with [" + tasksLeft.toString() + + "] before startAnimation()."); + } + + // Update local states. + setSplitsVisible(false); + // Wait until after animation to update divider + + if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { + // Reset crops so they don't interfere with subsequent launches + t.setWindowCrop(mMainStage.mRootLeash, null); + t.setWindowCrop(mSideStage.mRootLeash, null); + } + + if (mDismissTop == STAGE_TYPE_UNDEFINED) { + // Going home (dismissing both splits) + + // TODO: Have a proper remote for this. Until then, though, reset state and use the + // normal animation stuff (which falls back to the normal launcher remote). + t.hide(mSplitLayout.getDividerLeash()); + setDividerVisibility(false); + mSplitTransitions.mPendingDismiss = null; + return false; + } + + addDividerBarToTransition(info, t, false /* show */); + // We're dismissing split by moving the other one to fullscreen. + // Since we don't have any animations for this yet, just use the internal example + // animations. + return true; + } + + private void addDividerBarToTransition(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, boolean show) { + final SurfaceControl leash = mSplitLayout.getDividerLeash(); + final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash); + final Rect bounds = mSplitLayout.getDividerBounds(); + barChange.setStartAbsBounds(bounds); + barChange.setEndAbsBounds(bounds); + barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); + barChange.setFlags(FLAG_IS_DIVIDER_BAR); + // Technically this should be order-0, but this is running after layer assignment + // and it's a special case, so just add to end. + info.addChange(barChange); + // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. + if (show) { + t.setAlpha(leash, 1.f); + t.setLayer(leash, SPLIT_DIVIDER_LAYER); + t.setPosition(leash, bounds.left, bounds.top); + t.show(leash); + } + } + + RemoteAnimationTarget getDividerBarLegacyTarget() { + final Rect bounds = mSplitLayout.getDividerBounds(); + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + + RemoteAnimationTarget getOutlineLegacyTarget() { + final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds(); + // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to + // distinguish as a split auxiliary target in Launcher. + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + TAG + " mDisplayId=" + mDisplayId); + pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); + pw.println(innerPrefix + "MainStage"); + pw.println(childPrefix + "isActive=" + mMainStage.isActive()); + mMainStageListener.dump(pw, childPrefix); + pw.println(innerPrefix + "SideStage"); + mSideStageListener.dump(pw, childPrefix); + pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout); + } + + /** + * Directly set the visibility of both splits. This assumes hasChildren matches visibility. + * This is intended for batch use, so it assumes other state management logic is already + * handled. + */ + private void setSplitsVisible(boolean visible) { + mMainStageListener.mVisible = mSideStageListener.mVisible = visible; + mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; + } + + /** + * Sets drag info to be logged when splitscreen is next entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mLogger.enterRequestedByDrag(position, dragSessionId); + } + + /** + * Logs the exit of splitscreen. + */ + private void logExit(int exitReason) { + mLogger.logExit(exitReason, + SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, + SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + + /** + * Logs the exit of splitscreen to a specific stage. This must be called before the exit is + * executed. + */ + private void logExitToStage(int exitReason, boolean toMainStage) { + mLogger.logExit(exitReason, + toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED, + toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, + !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, + !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + + class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { + boolean mHasRootTask = false; + boolean mVisible = false; + boolean mHasChildren = false; + + @Override + public void onRootTaskAppeared() { + mHasRootTask = true; + StageCoordinator.this.onStageRootTaskAppeared(this); + } + + @Override + public void onStatusChanged(boolean visible, boolean hasChildren) { + if (!mHasRootTask) return; + + if (mHasChildren != hasChildren) { + mHasChildren = hasChildren; + StageCoordinator.this.onStageHasChildrenChanged(this); + } + if (mVisible != visible) { + mVisible = visible; + StageCoordinator.this.onStageVisibilityChanged(this); + } + } + + @Override + public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) { + StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible); + } + + @Override + public void onRootTaskVanished() { + reset(); + StageCoordinator.this.onStageRootTaskVanished(this); + } + + @Override + public void onNoLongerSupportMultiWindow() { + if (mMainStage.isActive()) { + StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, + SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW); + } + } + + private void reset() { + mHasRootTask = false; + mVisible = false; + mHasChildren = false; + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + "mHasRootTask=" + mHasRootTask); + pw.println(prefix + "mVisible=" + mVisible); + pw.println(prefix + "mHasChildren=" + mHasChildren); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java new file mode 100644 index 000000000000..8b36c9406b15 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; + +import android.annotation.CallSuper; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SurfaceUtils; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.io.PrintWriter; + +/** + * Base class that handle common task org. related for split-screen stages. + * Note that this class and its sub-class do not directly perform hierarchy operations. + * They only serve to hold a collection of tasks and provide APIs like + * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator} + * to perform operations in-sync with other containers. + * + * @see StageCoordinator + */ +class StageTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = StageTaskListener.class.getSimpleName(); + + protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD}; + protected static final int[] CONTROLLED_WINDOWING_MODES = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; + protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; + + /** Callback interface for listening to changes in a split-screen stage. */ + public interface StageListenerCallbacks { + void onRootTaskAppeared(); + + void onStatusChanged(boolean visible, boolean hasChildren); + + void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); + + void onRootTaskVanished(); + void onNoLongerSupportMultiWindow(); + } + + private final StageListenerCallbacks mCallbacks; + private final SurfaceSession mSurfaceSession; + protected final SyncTransactionQueue mSyncQueue; + + protected ActivityManager.RunningTaskInfo mRootTaskInfo; + protected SurfaceControl mRootLeash; + protected SurfaceControl mDimLayer; + protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>(); + private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>(); + + private final StageTaskUnfoldController mStageTaskUnfoldController; + + StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + mCallbacks = callbacks; + mSyncQueue = syncQueue; + mSurfaceSession = surfaceSession; + mStageTaskUnfoldController = stageTaskUnfoldController; + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + } + + int getChildCount() { + return mChildrenTaskInfo.size(); + } + + boolean containsTask(int taskId) { + return mChildrenTaskInfo.contains(taskId); + } + + /** + * Returns the top activity uid for the top child task. + */ + int getTopChildTaskUid() { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); + if (info.topActivityInfo == null) { + continue; + } + return info.topActivityInfo.applicationInfo.uid; + } + return 0; + } + + /** @return {@code true} if this listener contains the currently focused task. */ + boolean isFocused() { + if (mRootTaskInfo == null) { + return false; + } + + if (mRootTaskInfo.isFocused) { + return true; + } + + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + if (mChildrenTaskInfo.valueAt(i).isFocused) { + return true; + } + } + + return false; + } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mRootTaskInfo == null && !taskInfo.hasParentTask()) { + mRootLeash = leash; + mRootTaskInfo = taskInfo; + mCallbacks.onRootTaskAppeared(); + sendStatusChanged(); + mSyncQueue.runInSync(t -> { + t.hide(mRootLeash); + mDimLayer = + SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession); + }); + } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + final int taskId = taskInfo.taskId; + mChildrenLeashes.put(taskId, leash); + mChildrenTaskInfo.put(taskId, taskInfo); + updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); + mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible); + if (ENABLE_SHELL_TRANSITIONS) { + // Status is managed/synchronized by the transition lifecycle. + return; + } + sendStatusChanged(); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash); + } + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (!taskInfo.supportsMultiWindow) { + // Leave split screen if the task no longer supports multi window. + mCallbacks.onNoLongerSupportMultiWindow(); + return; + } + if (mRootTaskInfo.taskId == taskInfo.taskId) { + mRootTaskInfo = taskInfo; + } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); + mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */, + taskInfo.isVisible); + if (!ENABLE_SHELL_TRANSITIONS) { + updateChildTaskSurface( + taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */); + } + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + if (ENABLE_SHELL_TRANSITIONS) { + // Status is managed/synchronized by the transition lifecycle. + return; + } + sendStatusChanged(); + } + + @Override + @CallSuper + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + final int taskId = taskInfo.taskId; + if (mRootTaskInfo.taskId == taskId) { + mCallbacks.onRootTaskVanished(); + mSyncQueue.runInSync(t -> t.remove(mDimLayer)); + mRootTaskInfo = null; + } else if (mChildrenTaskInfo.contains(taskId)) { + mChildrenTaskInfo.remove(taskId); + mChildrenLeashes.remove(taskId); + mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); + if (ENABLE_SHELL_TRANSITIONS) { + // Status is managed/synchronized by the transition lifecycle. + return; + } + sendStatusChanged(); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskVanished(taskInfo); + } + } + + @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (mRootTaskInfo.taskId == taskId) { + b.setParent(mRootLeash); + } else if (mChildrenLeashes.contains(taskId)) { + b.setParent(mChildrenLeashes.get(taskId)); + } else { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + } + + void setBounds(Rect bounds, WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, bounds); + } + + void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { + if (!containsTask(taskId)) { + return; + } + wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */); + } + + void setVisibility(boolean visible, WindowContainerTransaction wct) { + wct.reorder(mRootTaskInfo.token, visible /* onTop */); + } + + void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, + @SplitScreen.StageType int stage) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + int taskId = mChildrenTaskInfo.keyAt(i); + listener.onTaskStageChanged(taskId, stage, + mChildrenTaskInfo.get(taskId).isVisible); + } + } + + private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash, boolean firstAppeared) { + final Point taskPositionInParent = taskInfo.positionInParent; + mSyncQueue.runInSync(t -> { + t.setWindowCrop(leash, null); + t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); + if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) { + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + } + }); + } + + private void sendStatusChanged() { + mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); + } + + @Override + @CallSuper + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java new file mode 100644 index 000000000000..62b9da6d4715 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.stagesplit; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.concurrent.Executor; + +/** + * Controls transformations of the split screen task surfaces in response + * to the unfolding/folding action on foldable devices + */ +public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener { + + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; + + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final ShellUnfoldProgressProvider mUnfoldProgressProvider; + private final DisplayInsetsController mDisplayInsetsController; + private final UnfoldBackgroundController mBackgroundController; + private final Executor mExecutor; + private final int mExpandedTaskBarHeight; + private final float mWindowCornerRadiusPx; + private final Rect mStageBounds = new Rect(); + private final TransactionPool mTransactionPool; + + private InsetsSource mTaskbarInsetsSource; + private boolean mBothStagesVisible; + + public StageTaskUnfoldController(@NonNull Context context, + @NonNull TransactionPool transactionPool, + @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, + @NonNull DisplayInsetsController displayInsetsController, + @NonNull UnfoldBackgroundController backgroundController, + @NonNull Executor executor) { + mUnfoldProgressProvider = unfoldProgressProvider; + mTransactionPool = transactionPool; + mExecutor = executor; + mBackgroundController = backgroundController; + mDisplayInsetsController = displayInsetsController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + /** + * Initializes the controller, starts listening for the external events + */ + public void init() { + mUnfoldProgressProvider.addListener(mExecutor, this); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + /** + * Called when split screen task appeared + * @param taskInfo info for the appeared task + * @param leash surface leash for the appeared task + */ + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + AnimationContext context = new AnimationContext(leash); + mAnimationContextByTaskId.put(taskInfo.taskId, context); + } + + /** + * Called when a split screen task vanished + * @param taskInfo info for the vanished task + */ + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); + if (context != null) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + resetSurface(transaction, context); + transaction.apply(); + mTransactionPool.release(transaction); + } + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + @Override + public void onStateChangeProgress(float progress) { + if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return; + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + mBackgroundController.ensureBackground(transaction); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + + context.mCurrentCropRect.set(RECT_EVALUATOR + .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); + + transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + + transaction.apply(); + + mTransactionPool.release(transaction); + } + + @Override + public void onStateChangeFinished() { + resetTransformations(); + } + + /** + * Called when split screen visibility changes + * @param bothStagesVisible true if both stages of the split screen are visible + */ + public void onSplitVisibilityChanged(boolean bothStagesVisible) { + mBothStagesVisible = bothStagesVisible; + if (!bothStagesVisible) { + resetTransformations(); + } + } + + /** + * Called when split screen stage bounds changed + * @param bounds new bounds for this stage + */ + public void onLayoutChanged(Rect bounds) { + mStageBounds.set(bounds); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + private void resetTransformations() { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(transaction, context); + } + mBackgroundController.removeBackground(transaction); + transaction.apply(); + + mTransactionPool.release(transaction); + } + + private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) { + transaction + .setWindowCrop(context.mLeash, null) + .setCornerRadius(context.mLeash, 0.0F); + } + + private class AnimationContext { + final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); + final Rect mEndCropRect = new Rect(); + final Rect mCurrentCropRect = new Rect(); + + private AnimationContext(SurfaceControl leash) { + this.mLeash = leash; + update(); + } + + private void update() { + mStartCropRect.set(mStageBounds); + + if (mTaskbarInsetsSource != null) { + // Only insets the cropping window with taskbar when taskbar is expanded + if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mStartCropRect.inset(mTaskbarInsetsSource + .calculateVisibleInsets(mStartCropRect)); + } + } + + // Offset to surface coordinates as layout bounds are in screen coordinates + mStartCropRect.offsetTo(0, 0); + + mEndCropRect.set(mStartCropRect); + + int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height()); + int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION); + mStartCropRect.inset(margin, margin, margin, margin); + } + } +} 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 29326ec90e31..8df7cbb27807 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 @@ -50,6 +50,7 @@ import android.os.Trace; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Slog; +import android.view.ContextThemeWrapper; import android.view.SurfaceControl; import android.view.View; import android.window.SplashScreenView; @@ -137,12 +138,14 @@ public class SplashscreenContentDrawer { * null if failed. */ void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info, - int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) { + int taskId, Consumer<SplashScreenView> splashScreenViewConsumer, + Consumer<Runnable> uiThreadInitConsumer) { mSplashscreenWorkerHandler.post(() -> { SplashScreenView contentView; try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView"); - contentView = makeSplashScreenContentView(context, info, suggestType); + contentView = makeSplashScreenContentView(context, info, suggestType, + uiThreadInitConsumer); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RuntimeException e) { Slog.w(TAG, "failed creating starting window content at taskId: " @@ -238,7 +241,7 @@ public class SplashscreenContentDrawer { } private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai, - @StartingWindowType int suggestType) { + @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) { updateDensity(); getWindowAttrs(context, mTmpAttrs); @@ -253,6 +256,7 @@ public class SplashscreenContentDrawer { .setWindowBGColor(themeBGColor) .overlayDrawable(legacyDrawable) .chooseStyle(suggestType) + .setUiThreadInitConsumer(uiThreadInitConsumer) .build(); } @@ -299,6 +303,11 @@ public class SplashscreenContentDrawer { } } + /** Creates the wrapper with system theme to avoid unexpected styles from app. */ + ContextThemeWrapper createViewContextWrapper(Context appContext) { + return new ContextThemeWrapper(appContext, mContext.getTheme()); + } + /** The configuration of the splash screen window. */ public static class SplashScreenWindowAttrs { private int mWindowBgResId = 0; @@ -318,6 +327,7 @@ public class SplashscreenContentDrawer { private int mThemeColor; private Drawable[] mFinalIconDrawables; private int mFinalIconSize = mIconSize; + private Consumer<Runnable> mUiThreadInitTask; StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { mContext = context; @@ -339,6 +349,11 @@ public class SplashscreenContentDrawer { return this; } + StartingWindowViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) { + mUiThreadInitTask = uiThreadInitTask; + return this; + } + SplashScreenView build() { Drawable iconDrawable; final int animationDuration; @@ -360,7 +375,7 @@ public class SplashscreenContentDrawer { createIconDrawable(iconDrawable, false); } else { final float iconScale = (float) mIconSize / (float) mDefaultIconSize; - final int densityDpi = mContext.getResources().getDisplayMetrics().densityDpi; + final int densityDpi = mContext.getResources().getConfiguration().densityDpi; final int scaledIconDpi = (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon"); @@ -385,7 +400,8 @@ public class SplashscreenContentDrawer { animationDuration = 0; } - return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration); + return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration, + mUiThreadInitTask); } private class ShapeIconFactory extends BaseIconFactory { @@ -463,7 +479,7 @@ public class SplashscreenContentDrawer { } private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable, - int animationDuration) { + int animationDuration, Consumer<Runnable> uiThreadInitTask) { Drawable foreground = null; Drawable background = null; if (iconDrawable != null) { @@ -472,13 +488,15 @@ public class SplashscreenContentDrawer { } Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon"); - final SplashScreenView.Builder builder = new SplashScreenView.Builder(mContext) + final ContextThemeWrapper wrapper = createViewContextWrapper(mContext); + final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper) .setBackgroundColor(mThemeColor) .setOverlayDrawable(mOverlayDrawable) .setIconSize(iconSize) .setIconBackground(background) .setCenterViewDrawable(foreground) - .setAnimationDurationMillis(animationDuration); + .setAnimationDurationMillis(animationDuration) + .setUiThreadInitConsumer(uiThreadInitTask); if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN && mTmpAttrs.mBrandingImage != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java index 951b97e791c9..38122ffc032b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java @@ -38,6 +38,7 @@ import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Trace; +import android.util.Log; import android.util.PathParser; import android.window.SplashScreenView; @@ -50,6 +51,8 @@ import com.android.internal.R; */ public class SplashscreenIconDrawableFactory { + private static final String TAG = "SplashscreenIconDrawableFactory"; + /** * @return An array containing the foreground drawable at index 0 and if needed a background * drawable at index 1. @@ -282,7 +285,12 @@ public class SplashscreenIconDrawableFactory { if (startListener != null) { startListener.run(); } - mAnimatableIcon.start(); + try { + mAnimatableIcon.start(); + } catch (Exception ex) { + Log.e(TAG, "Error while running the splash screen animated icon", ex); + animation.cancel(); + } } @Override @@ -304,6 +312,13 @@ public class SplashscreenIconDrawableFactory { return true; } + @Override + public void stopAnimation() { + if (mIconAnimator != null) { + mIconAnimator.end(); + } + } + private final Callback mCallback = new Callback() { @Override public void invalidateDrawable(@NonNull Drawable who) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java index 01c9b6630fa6..76105a39189b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java @@ -36,4 +36,12 @@ public interface StartingSurface { default int getBackgroundColor(TaskInfo taskInfo) { return Color.BLACK; } + + /** Set the proxy to communicate with SysUi side components. */ + void setSysuiProxy(SysuiProxy proxy); + + /** Callback to tell SysUi components execute some methods. */ + interface SysuiProxy { + void requestTopUi(boolean requestTopUi, String componentTag); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 147f5e30f9d6..979bf0056b72 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -37,7 +37,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PixelFormat; -import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.IBinder; import android.os.RemoteCallback; @@ -48,15 +47,16 @@ import android.util.Slog; import android.util.SparseArray; import android.view.Choreographer; import android.view.Display; -import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.widget.FrameLayout; import android.window.SplashScreenView; import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; +import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; import com.android.internal.R; @@ -115,6 +115,9 @@ public class StartingSurfaceDrawer { @VisibleForTesting final SplashscreenContentDrawer mSplashscreenContentDrawer; private Choreographer mChoreographer; + private final WindowManagerGlobal mWindowManagerGlobal; + private StartingSurface.SysuiProxy mSysuiProxy; + private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); /** * @param splashScreenExecutor The thread used to control add and remove starting window. @@ -126,6 +129,8 @@ public class StartingSurfaceDrawer { mSplashScreenExecutor = splashScreenExecutor; mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, pool); mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); + mWindowManagerGlobal = WindowManagerGlobal.getInstance(); + mDisplayManager.getDisplay(DEFAULT_DISPLAY); } private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); @@ -137,29 +142,21 @@ public class StartingSurfaceDrawer { private final SparseArray<SurfaceControlViewHost> mAnimatedSplashScreenSurfaceHosts = new SparseArray<>(1); - /** Obtain proper context for showing splash screen on the provided display. */ - private Context getDisplayContext(Context context, int displayId) { - if (displayId == DEFAULT_DISPLAY) { - // The default context fits. - return context; - } - - final Display targetDisplay = mDisplayManager.getDisplay(displayId); - if (targetDisplay == null) { - // Failed to obtain the non-default display where splash screen should be shown, - // lets not show at all. - return null; - } - - return context.createDisplayContext(targetDisplay); + private Display getDisplay(int displayId) { + return mDisplayManager.getDisplay(displayId); } - private int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { + int getSplashScreenTheme(int splashScreenThemeResId, ActivityInfo activityInfo) { return splashScreenThemeResId != 0 ? splashScreenThemeResId : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() : com.android.internal.R.style.Theme_DeviceDefault_DayNight; } + + void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { + mSysuiProxy = sysuiProxy; + } + /** * Called when a task need a splash screen starting window. * @@ -177,7 +174,7 @@ public class StartingSurfaceDrawer { final int displayId = taskInfo.displayId; final int taskId = taskInfo.taskId; - Context context = mContext; + // replace with the default theme if the application didn't set final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo); if (DEBUG_SPLASH_SCREEN) { @@ -185,14 +182,16 @@ public class StartingSurfaceDrawer { + " theme=" + Integer.toHexString(theme) + " task=" + taskInfo.taskId + " suggestType=" + suggestType); } - - // Obtain proper context to launch on the right display. - final Context displayContext = getDisplayContext(context, displayId); - if (displayContext == null) { + final Display display = getDisplay(displayId); + if (display == null) { // Can't show splash screen on requested display, so skip showing at all. return; } - context = displayContext; + Context context = displayId == DEFAULT_DISPLAY + ? mContext : mContext.createDisplayContext(display); + if (context == null) { + return; + } if (theme != context.getThemeResId()) { try { context = context.createPackageContextAsUser(activityInfo.packageName, @@ -303,7 +302,8 @@ public class StartingSurfaceDrawer { // Record whether create splash screen view success, notify to current thread after // create splash screen view finished. final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier(); - final FrameLayout rootLayout = new FrameLayout(context); + final FrameLayout rootLayout = new FrameLayout( + mSplashscreenContentDrawer.createViewContextWrapper(context)); rootLayout.setPadding(0, 0, 0, 0); rootLayout.setFitsSystemWindows(false); final Runnable setViewSynchronized = () -> { @@ -328,12 +328,13 @@ public class StartingSurfaceDrawer { } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); }; + if (mSysuiProxy != null) { + mSysuiProxy.requestTopUi(true, TAG); + } mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId, - viewSupplier::setView); - + viewSupplier::setView, viewSupplier::setUiThreadInitTask); try { - final WindowManager wm = context.getSystemService(WindowManager.class); - if (addWindow(taskId, appToken, rootLayout, wm, params, suggestType)) { + if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) { // We use the splash screen worker thread to create SplashScreenView while adding // the window, as otherwise Choreographer#doFrame might be delayed on this thread. // And since Choreographer#doFrame won't happen immediately after adding the window, @@ -366,6 +367,7 @@ public class StartingSurfaceDrawer { private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { private SplashScreenView mView; private boolean mIsViewSet; + private Runnable mUiThreadInitTask; void setView(SplashScreenView view) { synchronized (this) { mView = view; @@ -374,6 +376,12 @@ public class StartingSurfaceDrawer { } } + void setUiThreadInitTask(Runnable initTask) { + synchronized (this) { + mUiThreadInitTask = initTask; + } + } + @Override public @Nullable SplashScreenView get() { synchronized (this) { @@ -383,6 +391,10 @@ public class StartingSurfaceDrawer { } catch (InterruptedException ignored) { } } + if (mUiThreadInitTask != null) { + mUiThreadInitTask.run(); + mUiThreadInitTask = null; + } return mView; } } @@ -446,12 +458,13 @@ public class StartingSurfaceDrawer { /** * Called when the content of a task is ready to show, starting window can be removed. */ - public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId); + Slog.d(TAG, "Task start finish, remove starting surface for task " + + removalInfo.taskId); } - removeWindowSynced(taskId, leash, frame, playRevealAnimation); + removeWindowSynced(removalInfo); + } /** @@ -505,15 +518,17 @@ public class StartingSurfaceDrawer { Slog.v(TAG, reason + "the splash screen. Releasing SurfaceControlViewHost for task:" + taskId); } - viewHost.getView().post(viewHost::release); + SplashScreenView.releaseIconHost(viewHost); } - protected boolean addWindow(int taskId, IBinder appToken, View view, WindowManager wm, + protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, WindowManager.LayoutParams params, @StartingWindowType int suggestType) { boolean shouldSaveView = true; + final Context context = view.getContext(); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); - wm.addView(view, params); + mWindowManagerGlobal.addView(view, params, display, + null /* parentWindow */, context.getUserId()); } catch (WindowManager.BadTokenException e) { // ignore Slog.w(TAG, appToken + " already running, starting window not displayed. " @@ -521,9 +536,9 @@ public class StartingSurfaceDrawer { shouldSaveView = false; } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - if (view != null && view.getParent() == null) { + if (view.getParent() == null) { Slog.w(TAG, "view not successfully added to wm, removing view"); - wm.removeViewImmediate(view); + mWindowManagerGlobal.removeView(view, true /* immediate */); shouldSaveView = false; } } @@ -542,7 +557,8 @@ public class StartingSurfaceDrawer { } private void removeWindowNoAnimate(int taskId) { - removeWindowSynced(taskId, null, null, false); + mTmpRemovalInfo.taskId = taskId; + removeWindowSynced(mTmpRemovalInfo); } void onImeDrawnOnTask(int taskId) { @@ -554,8 +570,8 @@ public class StartingSurfaceDrawer { mStartingWindowRecords.remove(taskId); } - protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo) { + final int taskId = removalInfo.taskId; final StartingWindowRecord record = mStartingWindowRecords.get(taskId); if (record != null) { if (record.mDecorView != null) { @@ -566,9 +582,9 @@ public class StartingSurfaceDrawer { if (record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { removeWindowInner(record.mDecorView, false); } else { - if (playRevealAnimation) { + if (removalInfo.playRevealAnimation) { mSplashscreenContentDrawer.applyExitAnimation(record.mContentView, - leash, frame, + removalInfo.windowAnimationLeash, removalInfo.mainFrame, () -> removeWindowInner(record.mDecorView, true)); } else { // the SplashScreenView has been copied to client, hide the view to skip @@ -588,19 +604,19 @@ public class StartingSurfaceDrawer { Slog.v(TAG, "Removing task snapshot window for " + taskId); } record.mTaskSnapshotWindow.scheduleRemove( - () -> mStartingWindowRecords.remove(taskId)); + () -> mStartingWindowRecords.remove(taskId), removalInfo.deferRemoveForIme); } } } private void removeWindowInner(View decorView, boolean hideView) { + if (mSysuiProxy != null) { + mSysuiProxy.requestTopUi(false, TAG); + } if (hideView) { decorView.setVisibility(View.GONE); } - final WindowManager wm = decorView.getContext().getSystemService(WindowManager.class); - if (wm != null) { - wm.removeView(decorView); - } + mWindowManagerGlobal.removeView(decorView, false /* immediate */); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java index dee21b093dce..99644f9493d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java @@ -28,16 +28,14 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.graphics.Color; -import android.graphics.Rect; -import android.os.Build; import android.os.IBinder; import android.os.RemoteException; import android.os.Trace; import android.util.Slog; import android.util.SparseIntArray; -import android.view.SurfaceControl; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; +import android.window.StartingWindowRemovalInfo; import android.window.TaskOrganizer; import android.window.TaskSnapshot; @@ -68,7 +66,7 @@ import com.android.wm.shell.common.TransactionPool; public class StartingWindowController implements RemoteCallable<StartingWindowController> { private static final String TAG = StartingWindowController.class.getSimpleName(); - public static final boolean DEBUG_SPLASH_SCREEN = Build.isDebuggable(); + public static final boolean DEBUG_SPLASH_SCREEN = false; public static final boolean DEBUG_TASK_SNAPSHOT = false; private static final long TASK_BG_COLOR_RETAIN_TIME_MS = 5000; @@ -134,7 +132,7 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken, suggestionType); } else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) { - final TaskSnapshot snapshot = windowInfo.mTaskSnapshot; + final TaskSnapshot snapshot = windowInfo.taskSnapshot; mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, appToken, snapshot); } @@ -186,13 +184,12 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo /** * Called when the content of a task is ready to show, starting window can be removed. */ - public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow( - taskId, leash, frame, playRevealAnimation)); + removalInfo)); mSplashScreenExecutor.executeDelayed(() -> { synchronized (mTaskBackgroundColors) { - mTaskBackgroundColors.delete(taskId); + mTaskBackgroundColors.delete(removalInfo.taskId); } }, TASK_BG_COLOR_RETAIN_TIME_MS); } @@ -224,6 +221,11 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo return color != Color.TRANSPARENT ? color : SplashscreenContentDrawer.getSystemBGColor(); } + + @Override + public void setSysuiProxy(SysuiProxy proxy) { + mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.setSysuiProxy(proxy)); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index dfb1ae3ef2a0..3e88c464d359 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -130,7 +130,6 @@ public class TaskSnapshotWindow { private final Window mWindow; private final Runnable mClearWindowHandler; - private final long mDelayRemovalTime; private final ShellExecutor mSplashScreenExecutor; private final SurfaceControl mSurfaceControl; private final IWindowSession mSession; @@ -210,7 +209,7 @@ public class TaskSnapshotWindow { final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); - final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0]; + final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); final TaskDescription taskDescription; @@ -221,22 +220,19 @@ public class TaskSnapshotWindow { taskDescription.setBackgroundColor(WHITE); } - final long delayRemovalTime = snapshot.hasImeSurface() ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE - : DELAY_REMOVAL_TIME_GENERAL; - final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance, windowFlags, windowPrivateFlags, taskBounds, orientation, activityType, - delayRemovalTime, topWindowInsetsState, clearWindowHandler, splashScreenExecutor); + topWindowInsetsState, clearWindowHandler, splashScreenExecutor); final Window window = snapshotSurface.mWindow; - final InsetsState mTmpInsetsState = new InsetsState(); + final InsetsState tmpInsetsState = new InsetsState(); final InputChannel tmpInputChannel = new InputChannel(); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay"); final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, - mTmpInsetsState, tmpInputChannel, mTmpInsetsState, mTempControls); + info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); @@ -249,8 +245,8 @@ public class TaskSnapshotWindow { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1, - tmpFrames, tmpMergedConfiguration, surfaceControl, mTmpInsetsState, - mTempControls, TMP_SURFACE_SIZE); + tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, + tmpControls, TMP_SURFACE_SIZE); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); @@ -265,9 +261,8 @@ public class TaskSnapshotWindow { public TaskSnapshotWindow(SurfaceControl surfaceControl, TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription, int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds, - int currentOrientation, int activityType, long delayRemovalTime, - InsetsState topWindowInsetsState, Runnable clearWindowHandler, - ShellExecutor splashScreenExecutor) { + int currentOrientation, int activityType, InsetsState topWindowInsetsState, + Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) { mSplashScreenExecutor = splashScreenExecutor; mSession = WindowManagerGlobal.getWindowSession(); mWindow = new Window(); @@ -283,7 +278,6 @@ public class TaskSnapshotWindow { mStatusBarColor = taskDescription.getStatusBarColor(); mOrientationOnCreation = currentOrientation; mActivityType = activityType; - mDelayRemovalTime = delayRemovalTime; mTransaction = new SurfaceControl.Transaction(); mClearWindowHandler = clearWindowHandler; mHasImeSurface = snapshot.hasImeSurface(); @@ -294,7 +288,7 @@ public class TaskSnapshotWindow { } boolean hasImeSurface() { - return mHasImeSurface; + return mHasImeSurface; } /** @@ -314,7 +308,7 @@ public class TaskSnapshotWindow { mSystemBarBackgroundPainter.drawNavigationBarBackground(c); } - void scheduleRemove(Runnable onRemove) { + void scheduleRemove(Runnable onRemove, boolean deferRemoveForIme) { // Show the latest content as soon as possible for unlocking to home. if (mActivityType == ACTIVITY_TYPE_HOME) { removeImmediately(); @@ -329,9 +323,12 @@ public class TaskSnapshotWindow { TaskSnapshotWindow.this.removeImmediately(); onRemove.run(); }; - mSplashScreenExecutor.executeDelayed(mScheduledRunnable, mDelayRemovalTime); + final long delayRemovalTime = mHasImeSurface && deferRemoveForIme + ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE + : DELAY_REMOVAL_TIME_GENERAL; + mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); if (DEBUG) { - Slog.d(TAG, "Defer removing snapshot surface in " + mDelayRemovalTime); + Slog.d(TAG, "Defer removing snapshot surface in " + delayRemovalTime); } } @@ -362,7 +359,7 @@ public class TaskSnapshotWindow { static Rect getSystemBarInsets(Rect frame, InsetsState state) { return state.calculateInsets(frame, WindowInsets.Type.systemBars(), - false /* ignoreVisibility */); + false /* ignoreVisibility */).toRect(); } private void drawSnapshot() { @@ -382,6 +379,7 @@ public class TaskSnapshotWindow { // In case window manager leaks us, make sure we don't retain the snapshot. mSnapshot = null; + mSurfaceControl.release(); } private void drawSizeMatchSnapshot() { @@ -449,6 +447,7 @@ public class TaskSnapshotWindow { mTransaction.setBuffer(mSurfaceControl, background); } mTransaction.apply(); + childSurfaceControl.release(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java index 848eff4b56f3..bde2b5ff4d60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java @@ -71,23 +71,13 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor + " topIsHome:" + topIsHome); } - final int visibleSplashScreenType = legacySplashScreen - ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN - : STARTING_WINDOW_TYPE_SPLASH_SCREEN; - if (!topIsHome) { - if (!processRunning) { + if (!processRunning || newTask || (taskSwitch && !activityCreated)) { return useEmptySplashScreen ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN - : visibleSplashScreenType; - } - if (newTask) { - return useEmptySplashScreen - ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN - : visibleSplashScreenType; - } - if (taskSwitch && !activityCreated) { - return visibleSplashScreenType; + : legacySplashScreen + ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + : STARTING_WINDOW_TYPE_SPLASH_SCREEN; } } if (taskSwitch && allowTaskSnapshot) { @@ -107,7 +97,7 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor * rotation must be the same). */ private boolean isSnapshotCompatible(StartingWindowInfo windowInfo) { - final TaskSnapshot snapshot = windowInfo.mTaskSnapshot; + final TaskSnapshot snapshot = windowInfo.taskSnapshot; if (snapshot == null) { if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { Slog.d(TAG, "isSnapshotCompatible no snapshot " + windowInfo.taskInfo.taskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index c6fb5af7d4be..7abda994bb5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -16,18 +16,38 @@ package com.android.wm.shell.transition; +import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; +import static android.app.ActivityOptions.ANIM_CUSTOM; +import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; +import static android.app.ActivityOptions.ANIM_SCALE_UP; +import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; +import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static android.window.TransitionInfo.isIndependent; + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -35,25 +55,37 @@ import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; +import android.hardware.HardwareBuffer; import android.os.IBinder; +import android.os.SystemProperties; +import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Transformation; import android.window.TransitionInfo; +import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.AttributeCache; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.util.CounterRotator; import java.util.ArrayList; @@ -61,33 +93,179 @@ import java.util.ArrayList; public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static final int MAX_ANIMATION_DURATION = 3000; + /** + * Restrict ability of activities overriding transition animation in a way such that + * an activity can do it only when the transition happens within a same task. + * + * @see android.app.Activity#overridePendingTransition(int, int) + */ + private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = + "persist.wm.disable_custom_task_animation"; + + /** + * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY + */ + static boolean sDisableCustomTaskAnimationProperty = + SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); + private final TransactionPool mTransactionPool; + private final DisplayController mDisplayController; + private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; + private final SurfaceSession mSurfaceSession = new SurfaceSession(); + /** Keeps track of the currently-running animations associated with each transition. */ private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); private final Rect mInsets = new Rect(0, 0, 0, 0); private float mTransitionAnimationScaleSetting = 1.0f; - DefaultTransitionHandler(@NonNull TransactionPool transactionPool, Context context, + private final int mCurrentUserId; + + private ScreenRotationAnimation mRotationAnimation; + + DefaultTransitionHandler(@NonNull DisplayController displayController, + @NonNull TransactionPool transactionPool, Context context, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mDisplayController = displayController; mTransactionPool = transactionPool; + mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG); + mCurrentUserId = UserHandle.myUserId(); AttributeCache.init(context); } + @VisibleForTesting + static boolean isRotationSeamless(@NonNull TransitionInfo info, + DisplayController displayController) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Display is changing, check if it should be seamless."); + boolean checkedDisplayLayout = false; + boolean hasTask = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Only look at changing things. showing/hiding don't need to rotate. + if (change.getMode() != TRANSIT_CHANGE) continue; + + // This container isn't rotating, so we can ignore it. + if (change.getEndRotation() == change.getStartRotation()) continue; + + if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) { + // In the presence of System Alert windows we can not seamlessly rotate. + if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " display has system alert windows, so not seamless."); + return false; + } + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " wallpaper is participating but isn't seamless."); + return false; + } + } else if (change.getTaskInfo() != null) { + hasTask = true; + // We only enable seamless rotation if all the visible task windows requested it. + if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " task %s isn't requesting seamless, so not seamless.", + change.getTaskInfo().taskId); + return false; + } + + // This is the only way to get display-id currently, so we will check display + // capabilities here + if (!checkedDisplayLayout) { + // only need to check display once. + checkedDisplayLayout = true; + final DisplayLayout displayLayout = displayController.getDisplayLayout( + change.getTaskInfo().displayId); + // For the upside down rotation we don't rotate seamlessly as the navigation + // bar moves position. Note most apps (using orientation:sensor or user as + // opposed to fullSensor) will not enter the reverse portrait orientation, so + // actually the orientation won't change at all. + int upsideDownRotation = displayLayout.getUpsideDownRotation(); + if (change.getStartRotation() == upsideDownRotation + || change.getEndRotation() == upsideDownRotation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " rotation involves upside-down portrait, so not seamless."); + return false; + } + + // If the navigation bar can't change sides, then it will jump when we change + // orientations and we don't rotate seamlessly - unless that is allowed, eg. + // with gesture navigation where the navbar is low-profile enough that this + // isn't very noticeable. + if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() + && (!(displayLayout.navigationBarCanMove() + && (change.getStartAbsBounds().width() + != change.getStartAbsBounds().height())))) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " nav bar changes sides, so not seamless."); + return false; + } + } + } + } + + // ROTATION_ANIMATION_SEAMLESS can only be requested by task. + if (hasTask) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); + return true; + } + return false; + } + + /** + * Gets the rotation animation for the topmost task. Assumes that seamless is checked + * elsewhere, so it will default SEAMLESS to ROTATE. + */ + private int getRotationAnimation(@NonNull TransitionInfo info) { + // Traverse in top-to-bottom order so that the first task is top-most + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Only look at changing things. showing/hiding don't need to rotate. + if (change.getMode() != TRANSIT_CHANGE) continue; + + // This container isn't rotating, so we can ignore it. + if (change.getEndRotation() == change.getStartRotation()) continue; + + if (change.getTaskInfo() != null) { + final int anim = change.getRotationAnimation(); + if (anim == ROTATION_ANIMATION_UNSPECIFIED + // Fallback animation for seamless should also be default. + || anim == ROTATION_ANIMATION_SEAMLESS) { + return ROTATION_ANIMATION_ROTATE; + } + return anim; + } + } + return ROTATION_ANIMATION_ROTATE; + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "start default transition animation, info = %s", info); + // If keyguard goes away, we should loadKeyguardExitAnimation. Otherwise this just + // immediately finishes since there is no animation for screen-wake. + if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) { + startTransaction.apply(); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + return true; + } + if (mAnimations.containsKey(transition)) { throw new IllegalStateException("Got a duplicate startAnimation call for " + transition); @@ -95,21 +273,78 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final ArrayList<Animator> animations = new ArrayList<>(); mAnimations.put(transition, animations); + final ArrayMap<WindowContainerToken, CounterRotator> counterRotators = new ArrayMap<>(); + final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; + + for (int i = 0; i < counterRotators.size(); ++i) { + counterRotators.valueAt(i).cleanUp(info.getRootLeash()); + } + counterRotators.clear(); + + if (mRotationAnimation != null) { + mRotationAnimation.kill(); + mRotationAnimation = null; + } + mAnimations.remove(transition); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); }; + + final int wallpaperTransit = getWallpaperTransitType(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + + if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { + int rotateDelta = change.getEndRotation() - change.getStartRotation(); + int displayW = change.getEndAbsBounds().width(); + int displayH = change.getEndAbsBounds().height(); + if (info.getType() == TRANSIT_CHANGE) { + boolean isSeamless = isRotationSeamless(info, mDisplayController); + final int anim = getRotationAnimation(info); + if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) { + mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession, + mTransactionPool, startTransaction, change, info.getRootLeash()); + mRotationAnimation.startAnimation(animations, onAnimFinish, + mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor); + continue; + } + } else { + // opening/closing an app into a new orientation. Counter-rotate all + // "going-away" things since they are still in the old orientation. + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change innerChange = info.getChanges().get(j); + if (!Transitions.isClosingType(innerChange.getMode()) + || !isIndependent(innerChange, info) + || innerChange.getParent() == null) { + continue; + } + CounterRotator crot = counterRotators.get(innerChange.getParent()); + if (crot == null) { + crot = new CounterRotator(); + crot.setup(startTransaction, + info.getChange(innerChange.getParent()).getLeash(), + rotateDelta, displayW, displayH); + if (crot.getSurface() != null) { + int layer = info.getChanges().size() - j; + startTransaction.setLayer(crot.getSurface(), layer); + } + counterRotators.put(innerChange.getParent(), crot); + } + crot.addChild(startTransaction, innerChange.getLeash()); + } + } + } + if (change.getMode() == TRANSIT_CHANGE) { // No default animation for this, so just update bounds/position. - t.setPosition(change.getLeash(), + startTransaction.setPosition(change.getLeash(), change.getEndAbsBounds().left - change.getEndRelOffset().x, change.getEndAbsBounds().top - change.getEndRelOffset().y); if (change.getTaskInfo() != null) { // Skip non-tasks since those usually have null bounds. - t.setWindowCrop(change.getLeash(), + startTransaction.setWindowCrop(change.getLeash(), change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } } @@ -117,12 +352,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Don't animate anything that isn't independent. if (!TransitionInfo.isIndependent(change, info)) continue; - Animation a = loadAnimation(info.getType(), info.getFlags(), change); + Animation a = loadAnimation(info, change, wallpaperTransit); if (a != null) { - startAnimInternal(animations, a, change.getLeash(), onAnimFinish); + startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, + mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */); + + if (info.getAnimationOptions() != null) { + attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions()); + } } } - t.apply(); + startTransaction.apply(); + TransitionMetrics.getInstance().reportAnimationStart(transition); // run finish now in-case there are no animations onAnimFinish.run(); return true; @@ -141,87 +382,134 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @Nullable - private Animation loadAnimation(int type, int flags, TransitionInfo.Change change) { - // TODO(b/178678389): It should handle more type animation here + private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change, + int wallpaperTransit) { Animation a = null; - final boolean isOpening = Transitions.isOpeningType(type); + final int type = info.getType(); + final int flags = info.getFlags(); final int changeMode = change.getMode(); final int changeFlags = change.getFlags(); + final boolean isOpeningType = Transitions.isOpeningType(type); + final boolean enter = Transitions.isOpeningType(changeMode); + final boolean isTask = change.getTaskInfo() != null; + final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final int overrideType = options != null ? options.getType() : ANIM_NONE; + final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true; - if (type == TRANSIT_RELAUNCH) { - a = mTransitionAnimation.createRelaunchAnimation( - change.getStartAbsBounds(), mInsets, change.getEndAbsBounds()); - } else if (type == TRANSIT_KEYGUARD_GOING_AWAY) { + if (info.isKeyguardGoingAway()) { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, (changeFlags & FLAG_SHOW_WALLPAPER) != 0); } else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) { a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(); - } else if (changeMode == TRANSIT_OPEN && isOpening) { - if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - // This received a transferred starting window, so don't animate - return null; - } - - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */); - } else if (change.getTaskInfo() != null) { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskOpenEnterAnimation); - } else { - a = mTransitionAnimation.loadDefaultAnimationRes( - (changeFlags & FLAG_TRANSLUCENT) == 0 - ? R.anim.activity_open_enter : R.anim.activity_translucent_open_enter); - } - } else if (changeMode == TRANSIT_TO_FRONT && isOpening) { - if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - // This received a transferred starting window, so don't animate - return null; - } - - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */); - } else { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskToFrontEnterAnimation); - } - } else if (changeMode == TRANSIT_CLOSE && !isOpening) { - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */); - } else if (change.getTaskInfo() != null) { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskCloseExitAnimation); - } else { - a = mTransitionAnimation.loadDefaultAnimationRes( - (changeFlags & FLAG_TRANSLUCENT) == 0 - ? R.anim.activity_close_exit : R.anim.activity_translucent_close_exit); - } - } else if (changeMode == TRANSIT_TO_BACK && !isOpening) { - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */); + } else if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { + if (isOpeningType) { + a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter); } else { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskToBackExitAnimation); + a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter); } } else if (changeMode == TRANSIT_CHANGE) { // In the absence of a specific adapter, we just want to keep everything stationary. a = new AlphaAnimation(1.f, 1.f); a.setDuration(TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION); + } else if (type == TRANSIT_RELAUNCH) { + a = mTransitionAnimation.createRelaunchAnimation( + change.getEndAbsBounds(), mInsets, change.getEndAbsBounds()); + } else if (overrideType == ANIM_CUSTOM + && (canCustomContainer || options.getOverrideTaskTransition())) { + a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter + ? options.getEnterResId() : options.getExitResId()); + } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) { + a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(); + } else if (overrideType == ANIM_CLIP_REVEAL) { + a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter, + change.getEndAbsBounds(), change.getEndAbsBounds(), + options.getTransitionBounds()); + } else if (overrideType == ANIM_SCALE_UP) { + a = mTransitionAnimation.createScaleUpAnimationLocked(type, wallpaperTransit, enter, + change.getEndAbsBounds(), options.getTransitionBounds()); + } else if (overrideType == ANIM_THUMBNAIL_SCALE_UP + || overrideType == ANIM_THUMBNAIL_SCALE_DOWN) { + final boolean scaleUp = overrideType == ANIM_THUMBNAIL_SCALE_UP; + a = mTransitionAnimation.createThumbnailEnterExitAnimationLocked(enter, scaleUp, + change.getEndAbsBounds(), type, wallpaperTransit, options.getThumbnail(), + options.getTransitionBounds()); + } else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) { + // This received a transferred starting window, so don't animate + return null; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation); + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation); + } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperOpenExitAnimation); + } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperCloseExitAnimation); + } else if (type == TRANSIT_OPEN) { + if (isTask) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskOpenEnterAnimation + : R.styleable.WindowAnimation_taskOpenExitAnimation); + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { + a = mTransitionAnimation.loadDefaultAnimationRes( + R.anim.activity_translucent_open_enter); + } else { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_activityOpenEnterAnimation + : R.styleable.WindowAnimation_activityOpenExitAnimation); + } + } + } else if (type == TRANSIT_TO_FRONT) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskToFrontEnterAnimation + : R.styleable.WindowAnimation_taskToFrontExitAnimation); + } else if (type == TRANSIT_CLOSE) { + if (isTask) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskCloseEnterAnimation + : R.styleable.WindowAnimation_taskCloseExitAnimation); + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { + a = mTransitionAnimation.loadDefaultAnimationRes( + R.anim.activity_translucent_close_exit); + } else { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation); + } + } + } else if (type == TRANSIT_TO_BACK) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskToBackEnterAnimation + : R.styleable.WindowAnimation_taskToBackExitAnimation); } if (a != null) { - Rect start = change.getStartAbsBounds(); - Rect end = change.getEndAbsBounds(); + if (!a.isInitialized()) { + Rect end = change.getEndAbsBounds(); + a.initialize(end.width(), end.height(), end.width(), end.height()); + } a.restrictDuration(MAX_ANIMATION_DURATION); - a.initialize(end.width(), end.height(), start.width(), start.height()); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); } return a; } - private void startAnimInternal(@NonNull ArrayList<Animator> animations, @NonNull Animation anim, - @NonNull SurfaceControl leash, @NonNull Runnable finishCallback) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + static void startSurfaceAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Animation anim, @NonNull SurfaceControl leash, + @NonNull Runnable finishCallback, @NonNull TransactionPool pool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor, + @Nullable Point position) { + final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); final float[] matrix = new float[9]; @@ -231,14 +519,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { va.addUpdateListener(animation -> { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); - applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix); + applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, + position); }); final Runnable finisher = () -> { - applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix); + applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, + position); - mTransactionPool.release(transaction); - mMainExecutor.execute(() -> { + pool.release(transaction); + mainExecutor.execute(() -> { animations.remove(va); finishCallback.run(); }); @@ -255,12 +545,116 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } }); animations.add(va); - mAnimExecutor.execute(va::start); + animExecutor.execute(va::start); + } + + private void attachThumbnail(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, TransitionInfo.Change change, + TransitionInfo.AnimationOptions options) { + final boolean isTask = change.getTaskInfo() != null; + final boolean isOpen = Transitions.isOpeningType(change.getMode()); + final boolean isClose = Transitions.isClosingType(change.getMode()); + if (isOpen) { + if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) { + attachCrossProfileThunmbnailAnimation(animations, finishCallback, change); + } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) { + attachThumbnailAnimation(animations, finishCallback, change, options); + } + } else if (isClose && options.getType() == ANIM_THUMBNAIL_SCALE_DOWN) { + attachThumbnailAnimation(animations, finishCallback, change, options); + } + } + + private void attachCrossProfileThunmbnailAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, TransitionInfo.Change change) { + final int thumbnailDrawableRes = change.getTaskInfo().userId == mCurrentUserId + ? R.drawable.ic_account_circle : R.drawable.ic_corp_badge; + final Rect bounds = change.getEndAbsBounds(); + final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail( + thumbnailDrawableRes, bounds); + if (thumbnail == null) { + return; + } + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + change.getLeash(), thumbnail, transaction); + final Animation a = + mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds); + if (a == null) { + return; + } + + final Runnable finisher = () -> { + wt.destroy(transaction); + mTransactionPool.release(transaction); + + finishCallback.run(); + }; + a.restrictDuration(MAX_ANIMATION_DURATION); + a.scaleCurrentDuration(mTransitionAnimationScaleSetting); + startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, + mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top)); + } + + private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, TransitionInfo.Change change, + TransitionInfo.AnimationOptions options) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + change.getLeash(), options.getThumbnail(), transaction); + final Rect bounds = change.getEndAbsBounds(); + final int orientation = mContext.getResources().getConfiguration().orientation; + final Animation a = mTransitionAnimation.createThumbnailAspectScaleAnimationLocked(bounds, + mInsets, options.getThumbnail(), orientation, null /* startRect */, + options.getTransitionBounds(), options.getType() == ANIM_THUMBNAIL_SCALE_UP); + + final Runnable finisher = () -> { + wt.destroy(transaction); + mTransactionPool.release(transaction); + + finishCallback.run(); + }; + a.restrictDuration(MAX_ANIMATION_DURATION); + a.scaleCurrentDuration(mTransitionAnimationScaleSetting); + startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, + mMainExecutor, mAnimExecutor, null /* position */); + } + + private static int getWallpaperTransitType(TransitionInfo info) { + boolean hasOpenWallpaper = false; + boolean hasCloseWallpaper = false; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) { + if (Transitions.isOpeningType(change.getMode())) { + hasOpenWallpaper = true; + } else if (Transitions.isClosingType(change.getMode())) { + hasCloseWallpaper = true; + } + } + } + + if (hasOpenWallpaper && hasCloseWallpaper) { + return Transitions.isOpeningType(info.getType()) + ? WALLPAPER_TRANSITION_INTRA_OPEN : WALLPAPER_TRANSITION_INTRA_CLOSE; + } else if (hasOpenWallpaper) { + return WALLPAPER_TRANSITION_OPEN; + } else if (hasCloseWallpaper) { + return WALLPAPER_TRANSITION_CLOSE; + } else { + return WALLPAPER_TRANSITION_NONE; + } } private static void applyTransformation(long time, SurfaceControl.Transaction t, - SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix) { + SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix, + Point position) { anim.getTransformation(time, transformation); + if (position != null) { + transformation.getMatrix().postTranslate(position.x, position.y); + } t.setMatrix(leash, transformation.getMatrix(), matrix); t.setAlpha(leash, transformation.getAlpha()); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl index dffc700a3690..bdcdb63d2cd6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl @@ -16,7 +16,7 @@ package com.android.wm.shell.transition; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionFilter; /** @@ -28,10 +28,10 @@ interface IShellTransitions { * Registers a remote transition handler. */ oneway void registerRemote(in TransitionFilter filter, - in IRemoteTransition remoteTransition) = 1; + in RemoteTransition remoteTransition) = 1; /** * Unregisters a remote transition handler. */ - oneway void unregisterRemote(in IRemoteTransition remoteTransition) = 2; + oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java new file mode 100644 index 000000000000..61e11e877b90 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import android.annotation.NonNull; +import android.os.RemoteException; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.IWindowContainerTransactionCallback; + +/** + * Utilities and interfaces for transition-like usage on top of the legacy app-transition and + * synctransaction tools. + */ +public class LegacyTransitions { + + /** + * Interface for a "legacy" transition. Effectively wraps a sync callback + remoteAnimation + * into one callback. + */ + public interface ILegacyTransition { + /** + * Called when both the associated sync transaction finishes and the remote animation is + * ready. + */ + void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t); + } + + /** + * Makes sure that a remote animation and corresponding sync callback are called together + * such that the sync callback is called first. This assumes that both the callback receiver + * and the remoteanimation are in the same process so that order is preserved on both ends. + */ + public static class LegacyTransition { + private final ILegacyTransition mLegacyTransition; + private int mSyncId = -1; + private SurfaceControl.Transaction mTransaction; + private int mTransit; + private RemoteAnimationTarget[] mApps; + private RemoteAnimationTarget[] mWallpapers; + private RemoteAnimationTarget[] mNonApps; + private IRemoteAnimationFinishedCallback mFinishCallback = null; + private boolean mCancelled = false; + private final SyncCallback mSyncCallback = new SyncCallback(); + private final RemoteAnimationAdapter mAdapter = + new RemoteAnimationAdapter(new RemoteAnimationWrapper(), 0, 0); + + public LegacyTransition(@WindowManager.TransitionType int type, + @NonNull ILegacyTransition legacyTransition) { + mLegacyTransition = legacyTransition; + mTransit = type; + } + + public @WindowManager.TransitionType int getType() { + return mTransit; + } + + public IWindowContainerTransactionCallback getSyncCallback() { + return mSyncCallback; + } + + public RemoteAnimationAdapter getAdapter() { + return mAdapter; + } + + private class SyncCallback extends IWindowContainerTransactionCallback.Stub { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) + throws RemoteException { + mSyncId = id; + mTransaction = t; + checkApply(); + } + } + + private class RemoteAnimationWrapper extends IRemoteAnimationRunner.Stub { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { + mTransit = transit; + mApps = apps; + mWallpapers = wallpapers; + mNonApps = nonApps; + mFinishCallback = finishedCallback; + checkApply(); + } + + @Override + public void onAnimationCancelled() throws RemoteException { + mCancelled = true; + mApps = mWallpapers = mNonApps = null; + checkApply(); + } + } + + + private void checkApply() throws RemoteException { + if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) return; + mLegacyTransition.onAnimationStart(mTransit, mApps, mWallpapers, + mNonApps, mFinishCallback, mTransaction); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 4da6664aa3dc..3be896e4aca3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -24,6 +24,7 @@ import android.util.Log; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -43,10 +44,10 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { private IBinder mTransition = null; /** The remote to delegate animation to */ - private final IRemoteTransition mRemote; + private final RemoteTransition mRemote; public OneShotRemoteHandler(@NonNull ShellExecutor mainExecutor, - @NonNull IRemoteTransition remote) { + @NonNull RemoteTransition remote) { mMainExecutor = mainExecutor; mRemote = remote; } @@ -57,7 +58,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mTransition != transition) return false; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote" @@ -70,19 +72,24 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { }; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { if (mRemote.asBinder() != null) { mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } - mMainExecutor.execute( - () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); + mMainExecutor.execute(() -> { + if (sct != null) { + finishTransaction.merge(sct); + } + finishCallback.onTransitionFinished(wct, null /* wctCB */); + }); } }; try { if (mRemote.asBinder() != null) { mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - mRemote.startAnimation(transition, info, t, cb); + mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); if (mRemote.asBinder() != null) { @@ -102,13 +109,14 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { mMainExecutor.execute( () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); } }; try { - mRemote.mergeAnimation(transition, info, t, mergeTarget, cb); + mRemote.getRemoteTransition().mergeAnimation(transition, info, t, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error merging remote transition.", e); } @@ -118,8 +126,9 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { - IRemoteTransition remote = request.getRemoteTransition(); - if (remote != mRemote) return null; + RemoteTransition remote = request.getRemoteTransition(); + IRemoteTransition iRemote = remote != null ? remote.getRemoteTransition() : null; + if (iRemote != mRemote.getRemoteTransition()) return null; mTransition = transition; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" + " for %s: %s", transition, remote); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 9bfb261fcb85..c798ace18b5f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -27,6 +27,7 @@ import android.util.Slog; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -50,45 +51,33 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { private final ShellExecutor mMainExecutor; /** Includes remotes explicitly requested by, eg, ActivityOptions */ - private final ArrayMap<IBinder, IRemoteTransition> mRequestedRemotes = new ArrayMap<>(); + private final ArrayMap<IBinder, RemoteTransition> mRequestedRemotes = new ArrayMap<>(); /** Ordered by specificity. Last filters will be checked first */ - private final ArrayList<Pair<TransitionFilter, IRemoteTransition>> mFilters = + private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mFilters = new ArrayList<>(); - private final IBinder.DeathRecipient mTransitionDeathRecipient = - new IBinder.DeathRecipient() { - @Override - @BinderThread - public void binderDied() { - mMainExecutor.execute(() -> mFilters.clear()); - } - }; + private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>(); RemoteTransitionHandler(@NonNull ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; } - void addFiltered(TransitionFilter filter, IRemoteTransition remote) { - try { - remote.asBinder().linkToDeath(mTransitionDeathRecipient, 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to link to death"); - return; - } + void addFiltered(TransitionFilter filter, RemoteTransition remote) { + handleDeath(remote.asBinder(), null /* finishCallback */); mFilters.add(new Pair<>(filter, remote)); } - void removeFiltered(IRemoteTransition remote) { + void removeFiltered(RemoteTransition remote) { boolean removed = false; for (int i = mFilters.size() - 1; i >= 0; --i) { - if (mFilters.get(i).second == remote) { + if (mFilters.get(i).second.asBinder().equals(remote.asBinder())) { mFilters.remove(i); removed = true; } } if (removed) { - remote.asBinder().unlinkToDeath(mTransitionDeathRecipient, 0 /* flags */); + unhandleDeath(remote.asBinder(), null /* finishCallback */); } } @@ -99,9 +88,10 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - IRemoteTransition pendingRemote = mRequestedRemotes.get(transition); + RemoteTransition pendingRemote = mRequestedRemotes.get(transition); if (pendingRemote == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have " + "explicit remote, search filters for match for %s", transition, info); @@ -110,6 +100,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s", mFilters.get(i)); if (mFilters.get(i).first.matches(info)) { + Slog.d(TAG, "Found filter" + mFilters.get(i)); pendingRemote = mFilters.get(i).second; // Add to requested list so that it can be found for merge requests. mRequestedRemotes.put(transition, pendingRemote); @@ -122,36 +113,27 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { if (pendingRemote == null) return false; - final IRemoteTransition remote = pendingRemote; - final IBinder.DeathRecipient remoteDied = () -> { - Log.e(Transitions.TAG, "Remote transition died, finishing"); - mMainExecutor.execute(() -> { - mRequestedRemotes.remove(transition); - finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); - }); - }; + final RemoteTransition remote = pendingRemote; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { - if (remote.asBinder() != null) { - remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); - } + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { + unhandleDeath(remote.asBinder(), finishCallback); mMainExecutor.execute(() -> { + if (sct != null) { + finishTransaction.merge(sct); + } mRequestedRemotes.remove(transition); finishCallback.onTransitionFinished(wct, null /* wctCB */); }); } }; try { - if (remote.asBinder() != null) { - remote.asBinder().linkToDeath(remoteDied, 0 /* flags */); - } - remote.startAnimation(transition, info, t, cb); + handleDeath(remote.asBinder(), finishCallback); + remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); - if (remote.asBinder() != null) { - remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); - } + unhandleDeath(remote.asBinder(), finishCallback); mRequestedRemotes.remove(transition); mMainExecutor.execute( () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); @@ -163,14 +145,15 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget); + final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget).getRemoteTransition(); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s", transition, remote); if (remote == null) return; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { mMainExecutor.execute(() -> { if (!mRequestedRemotes.containsKey(mergeTarget)) { Log.e(TAG, "Merged transition finished after it's mergeTarget (the " @@ -193,11 +176,98 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { - IRemoteTransition remote = request.getRemoteTransition(); + RemoteTransition remote = request.getRemoteTransition(); if (remote == null) return null; mRequestedRemotes.put(transition, remote); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" + " for %s: %s", transition, remote); return new WindowContainerTransaction(); } + + private void handleDeath(@NonNull IBinder remote, + @Nullable Transitions.TransitionFinishCallback finishCallback) { + synchronized (mDeathHandlers) { + RemoteDeathHandler deathHandler = mDeathHandlers.get(remote); + if (deathHandler == null) { + deathHandler = new RemoteDeathHandler(remote); + try { + remote.linkToDeath(deathHandler, 0 /* flags */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death"); + return; + } + mDeathHandlers.put(remote, deathHandler); + } + deathHandler.addUser(finishCallback); + } + } + + private void unhandleDeath(@NonNull IBinder remote, + @Nullable Transitions.TransitionFinishCallback finishCallback) { + synchronized (mDeathHandlers) { + RemoteDeathHandler deathHandler = mDeathHandlers.get(remote); + if (deathHandler == null) return; + deathHandler.removeUser(finishCallback); + if (deathHandler.getUserCount() == 0) { + if (!deathHandler.mPendingFinishCallbacks.isEmpty()) { + throw new IllegalStateException("Unhandling death for binder that still has" + + " pending finishCallback(s)."); + } + remote.unlinkToDeath(deathHandler, 0 /* flags */); + mDeathHandlers.remove(remote); + } + } + } + + /** NOTE: binder deaths can alter the filter order */ + private class RemoteDeathHandler implements IBinder.DeathRecipient { + private final IBinder mRemote; + private final ArrayList<Transitions.TransitionFinishCallback> mPendingFinishCallbacks = + new ArrayList<>(); + private int mUsers = 0; + + RemoteDeathHandler(IBinder remote) { + mRemote = remote; + } + + void addUser(@Nullable Transitions.TransitionFinishCallback finishCallback) { + if (finishCallback != null) { + mPendingFinishCallbacks.add(finishCallback); + } + ++mUsers; + } + + void removeUser(@Nullable Transitions.TransitionFinishCallback finishCallback) { + if (finishCallback != null) { + mPendingFinishCallbacks.remove(finishCallback); + } + --mUsers; + } + + int getUserCount() { + return mUsers; + } + + @Override + @BinderThread + public void binderDied() { + mMainExecutor.execute(() -> { + for (int i = mFilters.size() - 1; i >= 0; --i) { + if (mRemote.equals(mFilters.get(i).second.asBinder())) { + mFilters.remove(i); + } + } + for (int i = mRequestedRemotes.size() - 1; i >= 0; --i) { + if (mRemote.equals(mRequestedRemotes.valueAt(i).asBinder())) { + mRequestedRemotes.removeAt(i); + } + } + for (int i = mPendingFinishCallbacks.size() - 1; i >= 0; --i) { + mPendingFinishCallbacks.get(i).onTransitionFinished( + null /* wct */, null /* wctCB */); + } + mPendingFinishCallbacks.clear(); + }); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java new file mode 100644 index 000000000000..13c670a1ab1e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import static android.hardware.HardwareBuffer.RGBA_8888; +import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT; +import static android.util.RotationUtils.deltaRotation; +import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE; + +import static com.android.wm.shell.transition.DefaultTransitionHandler.startSurfaceAnimation; +import static com.android.wm.shell.transition.Transitions.TAG; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorSpace; +import android.graphics.GraphicBuffer; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.ImageReader; +import android.util.Slog; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.SurfaceSession; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.window.TransitionInfo; + +import com.android.internal.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * This class handles the rotation animation when the device is rotated. + * + * <p> + * The screen rotation animation is composed of 4 different part: + * <ul> + * <li> The screenshot: <p> + * A screenshot of the whole screen prior the change of orientation is taken to hide the + * element resizing below. The screenshot is then animated to rotate and cross-fade to + * the new orientation with the content in the new orientation. + * + * <li> The windows on the display: <p>y + * Once the device is rotated, the screen and its content are in the new orientation. The + * animation first rotate the new content into the old orientation to then be able to + * animate to the new orientation + * + * <li> The Background color frame: <p> + * To have the animation seem more seamless, we add a color transitioning background behind the + * exiting and entering layouts. We compute the brightness of the start and end + * layouts and transition from the two brightness values as grayscale underneath the animation + * + * <li> The entering Blackframe: <p> + * The enter Blackframe is similar to the exit Blackframe but is only used when a custom + * rotation animation is used and matches the new content size instead of the screenshot. + * </ul> + */ +class ScreenRotationAnimation { + static final int MAX_ANIMATION_DURATION = 10 * 1000; + + private final Context mContext; + private final TransactionPool mTransactionPool; + private final float[] mTmpFloats = new float[9]; + // Complete transformations being applied. + private final Matrix mSnapshotInitialMatrix = new Matrix(); + /** The leash of display. */ + private final SurfaceControl mSurfaceControl; + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + + private final int mStartWidth; + private final int mStartHeight; + private final int mEndWidth; + private final int mEndHeight; + private final int mStartRotation; + private final int mEndRotation; + + /** This layer contains the actual screenshot that is to be faded out. */ + private SurfaceControl mScreenshotLayer; + /** + * Only used for screen rotation and not custom animations. Layered behind all other layers + * to avoid showing any "empty" spots + */ + private SurfaceControl mBackColorSurface; + /** The leash using to animate screenshot layer. */ + private SurfaceControl mAnimLeash; + private Transaction mTransaction; + + // The current active animation to move from the old to the new rotated + // state. Which animation is run here will depend on the old and new + // rotations. + private Animation mRotateExitAnimation; + private Animation mRotateEnterAnimation; + + /** Intensity of light/whiteness of the layout before rotation occurs. */ + private float mStartLuma; + /** Intensity of light/whiteness of the layout after rotation occurs. */ + private float mEndLuma; + + ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool, + Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash) { + mContext = context; + mTransactionPool = pool; + + mSurfaceControl = change.getLeash(); + mStartWidth = change.getStartAbsBounds().width(); + mStartHeight = change.getStartAbsBounds().height(); + mEndWidth = change.getEndAbsBounds().width(); + mEndHeight = change.getEndAbsBounds().height(); + mStartRotation = change.getStartRotation(); + mEndRotation = change.getEndRotation(); + + mStartBounds.set(change.getStartAbsBounds()); + mEndBounds.set(change.getEndAbsBounds()); + + mAnimLeash = new SurfaceControl.Builder(session) + .setParent(rootLeash) + .setEffectLayer() + .setCallsite("ShellRotationAnimation") + .setName("Animation leash of screenshot rotation") + .build(); + + try { + SurfaceControl.LayerCaptureArgs args = + new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl) + .setCaptureSecureLayers(true) + .setAllowProtected(true) + .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight)) + .build(); + SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = + SurfaceControl.captureLayers(args); + if (screenshotBuffer == null) { + Slog.w(TAG, "Unable to take screenshot of display"); + return; + } + + mBackColorSurface = new SurfaceControl.Builder(session) + .setParent(rootLeash) + .setColorLayer() + .setCallsite("ShellRotationAnimation") + .setName("BackColorSurface") + .build(); + + mScreenshotLayer = new SurfaceControl.Builder(session) + .setParent(mAnimLeash) + .setBLASTLayer() + .setSecure(screenshotBuffer.containsSecureLayers()) + .setCallsite("ShellRotationAnimation") + .setName("RotationLayer") + .build(); + + HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); + mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace()); + + GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer( + screenshotBuffer.getHardwareBuffer()); + + t.setLayer(mBackColorSurface, -1); + t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma}); + t.setAlpha(mBackColorSurface, 1); + t.show(mBackColorSurface); + + t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); + t.setPosition(mAnimLeash, 0, 0); + t.setAlpha(mAnimLeash, 1); + t.show(mAnimLeash); + + t.setBuffer(mScreenshotLayer, buffer); + t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace()); + t.show(mScreenshotLayer); + + } catch (Surface.OutOfResourcesException e) { + Slog.w(TAG, "Unable to allocate freeze surface", e); + } + + setRotation(t); + t.apply(); + } + + private void setRotation(SurfaceControl.Transaction t) { + // Compute the transformation matrix that must be applied + // to the snapshot to make it stay in the same original position + // with the current screen rotation. + int delta = deltaRotation(mEndRotation, mStartRotation); + createRotationMatrix(delta, mStartWidth, mStartHeight, mSnapshotInitialMatrix); + setRotationTransform(t, mSnapshotInitialMatrix); + } + + private void setRotationTransform(SurfaceControl.Transaction t, Matrix matrix) { + if (mScreenshotLayer == null) { + return; + } + matrix.getValues(mTmpFloats); + float x = mTmpFloats[Matrix.MTRANS_X]; + float y = mTmpFloats[Matrix.MTRANS_Y]; + t.setPosition(mScreenshotLayer, x, y); + t.setMatrix(mScreenshotLayer, + mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y], + mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]); + + t.setAlpha(mScreenshotLayer, (float) 1.0); + t.show(mScreenshotLayer); + } + + /** + * Returns true if animating. + */ + public boolean startAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, float animationScale, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + if (mScreenshotLayer == null) { + // Can't do animation. + return false; + } + + // TODO : Found a way to get right end luma and re-enable color frame animation. + // End luma value is very not stable so it will cause more flicker is we run background + // color frame animation. + //mEndLuma = getLumaOfSurfaceControl(mEndBounds, mSurfaceControl); + + // Figure out how the screen has moved from the original rotation. + int delta = deltaRotation(mEndRotation, mStartRotation); + switch (delta) { /* Counter-Clockwise Rotations */ + case Surface.ROTATION_0: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_0_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.rotation_animation_enter); + break; + case Surface.ROTATION_90: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_plus_90_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_plus_90_enter); + break; + case Surface.ROTATION_180: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_180_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_180_enter); + break; + case Surface.ROTATION_270: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_minus_90_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_minus_90_enter); + break; + } + + mRotateExitAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight); + mRotateExitAnimation.restrictDuration(MAX_ANIMATION_DURATION); + mRotateExitAnimation.scaleCurrentDuration(animationScale); + mRotateEnterAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight); + mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION); + mRotateEnterAnimation.scaleCurrentDuration(animationScale); + + mTransaction = mTransactionPool.acquire(); + startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor); + startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, animExecutor); + //startColorAnimation(mTransaction, animationScale); + + return true; + } + + private void startDisplayRotation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, + @NonNull ShellExecutor animExecutor) { + startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, + mTransactionPool, mainExecutor, animExecutor, null /* position */); + } + + private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, + @NonNull ShellExecutor animExecutor) { + startSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, + mTransactionPool, mainExecutor, animExecutor, null /* position */); + } + + private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { + int colorTransitionMs = mContext.getResources().getInteger( + R.integer.config_screen_rotation_color_transition); + final float[] rgbTmpFloat = new float[3]; + final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma); + final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma); + final long duration = colorTransitionMs * (long) animationScale; + final Transaction t = mTransactionPool.acquire(); + + final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + // Animation length is already expected to be scaled. + va.overrideDurationScale(1.0f); + va.setDuration(duration); + va.addUpdateListener(animation -> { + final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); + final float fraction = currentPlayTime / va.getDuration(); + applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, + t); + mTransactionPool.release(t); + } + + @Override + public void onAnimationEnd(Animator animation) { + applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, + t); + mTransactionPool.release(t); + } + }); + animExecutor.execute(va::start); + } + + public void kill() { + Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire(); + if (mAnimLeash.isValid()) { + t.remove(mAnimLeash); + } + + if (mScreenshotLayer != null) { + if (mScreenshotLayer.isValid()) { + t.remove(mScreenshotLayer); + } + mScreenshotLayer = null; + + if (mBackColorSurface != null) { + if (mBackColorSurface.isValid()) { + t.remove(mBackColorSurface); + } + mBackColorSurface = null; + } + } + t.apply(); + mTransactionPool.release(t); + } + + /** + * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the + * luminance at the borders of the bitmap + * @return the average luminance of all the pixels at the borders of the bitmap + */ + private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) { + // Cannot read content from buffer with protected usage. + if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888 + || hasProtectedContent(hardwareBuffer)) { + return 0; + } + + ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(), + hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1); + ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace); + Image image = ir.acquireLatestImage(); + if (image == null || image.getPlanes().length == 0) { + return 0; + } + + Image.Plane plane = image.getPlanes()[0]; + ByteBuffer buffer = plane.getBuffer(); + int width = image.getWidth(); + int height = image.getHeight(); + int pixelStride = plane.getPixelStride(); + int rowStride = plane.getRowStride(); + float[] borderLumas = new float[2 * width + 2 * height]; + + // Grab the top and bottom borders + int l = 0; + for (int x = 0; x < width; x++) { + borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride); + borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride); + } + + // Grab the left and right borders + for (int y = 0; y < height; y++) { + borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride); + borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride); + } + + // Cleanup + ir.close(); + + // Oh, is this too simple and inefficient for you? + // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians + Arrays.sort(borderLumas); + return borderLumas[borderLumas.length / 2]; + } + + /** + * @return whether the hardwareBuffer passed in is marked as protected. + */ + private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) { + return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT; + } + + private static float getPixelLuminance(ByteBuffer buffer, int x, int y, + int pixelStride, int rowStride) { + int offset = y * rowStride + x * pixelStride; + int pixel = 0; + pixel |= (buffer.get(offset) & 0xff) << 16; // R + pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G + pixel |= (buffer.get(offset + 2) & 0xff); // B + pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A + return Color.valueOf(pixel).luminance(); + } + + /** + * Gets the average border luma by taking a screenshot of the {@param surfaceControl}. + * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace) + */ + private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) { + if (surfaceControl == null) { + return 0; + } + + Rect crop = new Rect(0, 0, bounds.width(), bounds.height()); + SurfaceControl.ScreenshotHardwareBuffer buffer = + SurfaceControl.captureLayers(surfaceControl, crop, 1); + if (buffer == null) { + return 0; + } + + return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace()); + } + + private static void createRotationMatrix(int rotation, int width, int height, + Matrix outMatrix) { + switch (rotation) { + case Surface.ROTATION_0: + outMatrix.reset(); + break; + case Surface.ROTATION_90: + outMatrix.setRotate(90, 0, 0); + outMatrix.postTranslate(height, 0); + break; + case Surface.ROTATION_180: + outMatrix.setRotate(180, 0, 0); + outMatrix.postTranslate(width, height); + break; + case Surface.ROTATION_270: + outMatrix.setRotate(270, 0, 0); + outMatrix.postTranslate(0, width); + break; + } + } + + private static void applyColor(int startColor, int endColor, float[] rgbFloat, + float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { + final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, + endColor); + Color middleColor = Color.valueOf(color); + rgbFloat[0] = middleColor.red(); + rgbFloat[1] = middleColor.green(); + rgbFloat[2] = middleColor.blue(); + if (surface.isValid()) { + t.setColor(surface, rgbFloat); + } + t.apply(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java index bc42c6e2f12c..802d25f66340 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java @@ -17,7 +17,7 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionFilter; import com.android.wm.shell.common.annotations.ExternalThread; @@ -39,10 +39,10 @@ public interface ShellTransitions { * Registers a remote transition. */ void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition); + @NonNull RemoteTransition remoteTransition); /** * Unregisters a remote transition. */ - void unregisterRemote(@NonNull IRemoteTransition remoteTransition); + void unregisterRemote(@NonNull RemoteTransition remoteTransition); } 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 60707ccdca30..c36983189a71 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.transition; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; +import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -39,10 +40,11 @@ import android.provider.Settings; import android.util.Log; import android.view.SurfaceControl; import android.view.WindowManager; -import android.window.IRemoteTransition; import android.window.ITransitionPlayer; +import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; +import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; @@ -54,6 +56,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; @@ -77,6 +80,15 @@ public class Transitions implements RemoteCallable<Transitions> { /** Transition type for launching 2 tasks simultaneously. */ public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 2; + /** Transition type for exiting PIP via the Shell, via pressing the expand button. */ + public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 3; + + /** Transition type for removing PIP via the Shell, either via Dismiss bubble or Close. */ + public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 4; + + /** Transition type for entering split by opening an app into side-stage. */ + public static final int TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE = TRANSIT_FIRST_CUSTOM + 5; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; @@ -91,27 +103,29 @@ public class Transitions implements RemoteCallable<Transitions> { private float mTransitionAnimationScaleSetting = 1.0f; private static final class ActiveTransition { - IBinder mToken = null; - TransitionHandler mHandler = null; - boolean mMerged = false; - TransitionInfo mInfo = null; - SurfaceControl.Transaction mStartT = null; - SurfaceControl.Transaction mFinishT = null; + IBinder mToken; + TransitionHandler mHandler; + boolean mMerged; + boolean mAborted; + TransitionInfo mInfo; + SurfaceControl.Transaction mStartT; + SurfaceControl.Transaction mFinishT; } /** Keeps track of currently playing transitions in the order of receipt. */ private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>(); public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, - @NonNull Context context, @NonNull ShellExecutor mainExecutor, - @NonNull ShellExecutor animExecutor) { + @NonNull DisplayController displayController, @NonNull Context context, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mPlayerImpl = new TransitionPlayerImpl(); // The very last handler (0 in the list) should be the default one. - mHandlers.add(new DefaultTransitionHandler(pool, context, mainExecutor, animExecutor)); + mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor, + animExecutor)); // Next lowest priority is remote transitions. mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor); mHandlers.add(mRemoteTransitionHandler); @@ -163,13 +177,13 @@ public class Transitions implements RemoteCallable<Transitions> { return new ShellTransitions() { @Override public void registerRemote(@androidx.annotation.NonNull TransitionFilter filter, - @androidx.annotation.NonNull IRemoteTransition remoteTransition) { + @androidx.annotation.NonNull RemoteTransition remoteTransition) { // Do nothing } @Override public void unregisterRemote( - @androidx.annotation.NonNull IRemoteTransition remoteTransition) { + @androidx.annotation.NonNull RemoteTransition remoteTransition) { // Do nothing } }; @@ -179,6 +193,8 @@ public class Transitions implements RemoteCallable<Transitions> { public void register(ShellTaskOrganizer taskOrganizer) { if (mPlayerImpl == null) return; taskOrganizer.registerTransitionPlayer(mPlayerImpl); + // Pre-load the instance. + TransitionMetrics.getInstance(); } /** @@ -205,12 +221,12 @@ public class Transitions implements RemoteCallable<Transitions> { /** Register a remote transition to be used when `filter` matches an incoming transition */ public void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition) { + @NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.addFiltered(filter, remoteTransition); } /** Unregisters a remote transition and all associated filters */ - public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.removeFiltered(remoteTransition); } @@ -218,7 +234,7 @@ public class Transitions implements RemoteCallable<Transitions> { public static boolean isOpeningType(@WindowManager.TransitionType int type) { return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT - || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; + || type == TRANSIT_KEYGUARD_GOING_AWAY; } /** @return true if the transition was triggered by closing something vs opening something */ @@ -382,7 +398,7 @@ public class Transitions implements RemoteCallable<Transitions> { } boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) { - return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, + return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb)); } @@ -416,17 +432,19 @@ public class Transitions implements RemoteCallable<Transitions> { /** Special version of finish just for dealing with no-op/invalid transitions. */ private void onAbort(IBinder transition) { - final int activeIdx = findActiveTransition(transition); - if (activeIdx < 0) return; - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Transition animation aborted due to no-op, notifying core %s", transition); - mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(transition, null /* wct */, null /* wctCB */); + onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */); } private void onFinish(IBinder transition, @Nullable WindowContainerTransaction wct, @Nullable WindowContainerTransactionCallback wctCB) { + onFinish(transition, wct, wctCB, false /* abort */); + } + + private void onFinish(IBinder transition, + @Nullable WindowContainerTransaction wct, + @Nullable WindowContainerTransactionCallback wctCB, + boolean abort) { int activeIdx = findActiveTransition(transition); if (activeIdx < 0) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " @@ -434,28 +452,37 @@ public class Transitions implements RemoteCallable<Transitions> { return; } else if (activeIdx > 0) { // This transition was merged. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s", - transition); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:" + + " %s", abort, transition); final ActiveTransition active = mActiveTransitions.get(activeIdx); active.mMerged = true; + active.mAborted = abort; if (active.mHandler != null) { active.mHandler.onTransitionMerged(active.mToken); } return; } + mActiveTransitions.get(activeIdx).mAborted = abort; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Transition animation finished, notifying core %s", transition); + "Transition animation finished (abort=%b), notifying core %s", abort, transition); // Merge all relevant transactions together SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT; for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { final ActiveTransition toMerge = mActiveTransitions.get(iA); if (!toMerge.mMerged) break; + // aborted transitions have no start/finish transactions + if (mActiveTransitions.get(iA).mStartT == null) break; + if (fullFinish == null) { + fullFinish = new SurfaceControl.Transaction(); + } // Include start. It will be a no-op if it was already applied. Otherwise, we need it // to maintain consistent state. fullFinish.merge(mActiveTransitions.get(iA).mStartT); fullFinish.merge(mActiveTransitions.get(iA).mFinishT); } - fullFinish.apply(); + if (fullFinish != null) { + fullFinish.apply(); + } // Now perform all the finishes. mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(transition, wct, wctCB); @@ -464,6 +491,12 @@ public class Transitions implements RemoteCallable<Transitions> { ActiveTransition merged = mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); } + // sift through aborted transitions + while (mActiveTransitions.size() > activeIdx + && mActiveTransitions.get(activeIdx).mAborted) { + ActiveTransition aborted = mActiveTransitions.remove(activeIdx); + mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); + } if (mActiveTransitions.size() <= activeIdx) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " + "finished"); @@ -494,6 +527,12 @@ public class Transitions implements RemoteCallable<Transitions> { int mergeIdx = activeIdx + 1; while (mergeIdx < mActiveTransitions.size()) { ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx); + if (mergeCandidate.mAborted) { + // transition was aborted, so we can skip for now (still leave it in the list + // so that it gets cleaned-up in the right order). + ++mergeIdx; + continue; + } if (mergeCandidate.mMerged) { throw new IllegalStateException("Can't merge a transition after not-merging" + " a preceding one."); @@ -566,12 +605,19 @@ public class Transitions implements RemoteCallable<Transitions> { * Starts a transition animation. This is always called if handleRequest returned non-null * for a particular transition. Otherwise, it is only called if no other handler before * it handled the transition. - * + * @param startTransaction the transaction given to the handler to be applied before the + * transition animation. Note the handler is expected to call on + * {@link SurfaceControl.Transaction#apply()} for startTransaction. + * @param finishTransaction the transaction given to the handler to be applied after the + * transition animation. Unlike startTransaction, the handler is NOT + * expected to apply this transaction. The Transition system will + * apply it when finishCallback is called. * @param finishCallback Call this when finished. This MUST be called on main thread. * @return true if transition was handled, false if not (falls-back to default). */ boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback); /** @@ -661,14 +707,14 @@ public class Transitions implements RemoteCallable<Transitions> { @Override public void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition) { + @NonNull RemoteTransition remoteTransition) { mMainExecutor.execute(() -> { mRemoteTransitionHandler.addFiltered(filter, remoteTransition); }); } @Override - public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { mMainExecutor.execute(() -> { mRemoteTransitionHandler.removeFiltered(remoteTransition); }); @@ -695,7 +741,7 @@ public class Transitions implements RemoteCallable<Transitions> { @Override public void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition) { + @NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "registerRemote", (transitions) -> { transitions.mRemoteTransitionHandler.addFiltered(filter, remoteTransition); @@ -703,7 +749,7 @@ public class Transitions implements RemoteCallable<Transitions> { } @Override - public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "unregisterRemote", (transitions) -> { transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java new file mode 100644 index 000000000000..2c668ed3d84d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.transition; + +import android.graphics.ColorSpace; +import android.graphics.GraphicBuffer; +import android.graphics.PixelFormat; +import android.hardware.HardwareBuffer; +import android.view.SurfaceControl; +import android.view.SurfaceSession; + +/** + * Represents a surface that is displayed over a transition surface. + */ +class WindowThumbnail { + + private SurfaceControl mSurfaceControl; + + private WindowThumbnail() {} + + /** Create a thumbnail surface and attach it over a parent surface. */ + static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent, + HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) { + WindowThumbnail windowThumbnail = new WindowThumbnail(); + windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession) + .setParent(parent) + .setName("WindowThumanil : " + parent.toString()) + .setCallsite("WindowThumanil") + .setFormat(PixelFormat.TRANSLUCENT) + .build(); + + GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(thumbnailHeader); + t.setBuffer(windowThumbnail.mSurfaceControl, graphicBuffer); + t.setColorSpace(windowThumbnail.mSurfaceControl, ColorSpace.get(ColorSpace.Named.SRGB)); + t.setLayer(windowThumbnail.mSurfaceControl, Integer.MAX_VALUE); + t.show(windowThumbnail.mSurfaceControl); + t.apply(); + + return windowThumbnail; + } + + SurfaceControl getSurface() { + return mSurfaceControl; + } + + /** Remove the thumbnail surface and release the surface. */ + void destroy(SurfaceControl.Transaction t) { + if (mSurfaceControl == null) { + return; + } + + t.remove(mSurfaceControl); + t.apply(); + mSurfaceControl.release(); + mSurfaceControl = null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java new file mode 100644 index 000000000000..74e48120bf1a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.unfold; + +import android.annotation.FloatRange; + +import java.util.concurrent.Executor; + +/** + * Wrapper interface for unfold transition progress provider for the Shell + * @see com.android.systemui.unfold.UnfoldTransitionProgressProvider + */ +public interface ShellUnfoldProgressProvider { + + /** + * Adds a transition listener + */ + void addListener(Executor executor, UnfoldListener listener); + + /** + * Listener for receiving unfold updates + */ + interface UnfoldListener { + default void onStateChangeStarted() {} + + default void onStateChangeProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {} + + default void onStateChangeFinished() {} + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java new file mode 100644 index 000000000000..9faf454261d3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.unfold; + +import static android.graphics.Color.blue; +import static android.graphics.Color.green; +import static android.graphics.Color.red; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.annotation.NonNull; +import android.content.Context; +import android.view.SurfaceControl; + +import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; + +/** + * Controls background color layer for the unfold animations + */ +public class UnfoldBackgroundController { + + private static final int BACKGROUND_LAYER_Z_INDEX = -1; + + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final float[] mBackgroundColor; + private SurfaceControl mBackgroundLayer; + + public UnfoldBackgroundController( + @NonNull Context context, + @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mBackgroundColor = getBackgroundColor(context); + } + + /** + * Ensures that unfold animation background color layer is present, + * @param transaction where we should add the background if it is not added + */ + public void ensureBackground(@NonNull SurfaceControl.Transaction transaction) { + if (mBackgroundLayer != null) return; + + SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder() + .setName("app-unfold-background") + .setCallsite("AppUnfoldTransitionController") + .setColorLayer(); + mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder); + mBackgroundLayer = colorLayerBuilder.build(); + + transaction + .setColor(mBackgroundLayer, mBackgroundColor) + .show(mBackgroundLayer) + .setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX); + } + + /** + * Ensures that the background is not visible + * @param transaction as part of which the removal will happen if needed + */ + public void removeBackground(@NonNull SurfaceControl.Transaction transaction) { + if (mBackgroundLayer == null) return; + if (mBackgroundLayer.isValid()) { + transaction.remove(mBackgroundLayer); + } + mBackgroundLayer = null; + } + + private float[] getBackgroundColor(Context context) { + int colorInt = context.getResources().getColor(R.color.unfold_transition_background); + return new float[]{ + (float) red(colorInt) / 255.0F, + (float) green(colorInt) / 255.0F, + (float) blue(colorInt) / 255.0F + }; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java new file mode 100644 index 000000000000..b9b671635010 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.util; + +import android.view.SurfaceControl; + +import java.util.ArrayList; + +/** + * Utility class that takes care of counter-rotating surfaces during a transition animation. + */ +public class CounterRotator { + SurfaceControl mSurface = null; + ArrayList<SurfaceControl> mRotateChildren = null; + + /** Gets the surface with the counter-rotation. */ + public SurfaceControl getSurface() { + return mSurface; + } + + /** + * Sets up this rotator. + * + * @param rotateDelta is the forward rotation change (the rotation the display is making). + * @param displayW (and H) Is the size of the rotating display. + */ + public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta, + float displayW, float displayH) { + if (rotateDelta == 0) return; + mRotateChildren = new ArrayList<>(); + // We want to counter-rotate, so subtract from 4 + rotateDelta = 4 - (rotateDelta + 4) % 4; + mSurface = new SurfaceControl.Builder() + .setName("Transition Unrotate") + .setContainerLayer() + .setParent(parent) + .build(); + // column-major + if (rotateDelta == 1) { + t.setMatrix(mSurface, 0, 1, -1, 0); + t.setPosition(mSurface, displayW, 0); + } else if (rotateDelta == 2) { + t.setMatrix(mSurface, -1, 0, 0, -1); + t.setPosition(mSurface, displayW, displayH); + } else if (rotateDelta == 3) { + t.setMatrix(mSurface, 0, -1, 1, 0); + t.setPosition(mSurface, 0, displayH); + } + t.show(mSurface); + } + + /** + * Add a surface that needs to be counter-rotate. + */ + public void addChild(SurfaceControl.Transaction t, SurfaceControl child) { + if (mSurface == null) return; + t.reparent(child, mSurface); + mRotateChildren.add(child); + } + + /** + * Clean-up. This undoes any reparenting and effectively stops the counter-rotation. + */ + public void cleanUp(SurfaceControl rootLeash) { + if (mSurface == null) return; + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (int i = mRotateChildren.size() - 1; i >= 0; --i) { + t.reparent(mRotateChildren.get(i), rootLeash); + } + t.remove(mSurface); + t.apply(); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 9dd25fe0e6fe..3ca5b9c38aff 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -25,11 +25,17 @@ package { android_test { name: "WMShellFlickerTests", - srcs: ["src/**/*.java", "src/**/*.kt"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], manifest: "AndroidManifest.xml", test_config: "AndroidTest.xml", platform_apis: true, certificate: "platform", + optimize: { + enabled: false, + }, test_suites: ["device-tests"], libs: ["android.test.runner"], static_libs: [ diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml index e6d32ff1166f..06df9568e01a 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml @@ -42,6 +42,9 @@ <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> <!-- ATM.removeRootTasksWithActivityTypes() --> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Enable bubble notification--> + <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <!-- Allow the test to write directly to /sdcard/ --> <application android:requestLegacyExternalStorage="true"> <uses-library android:name="android.test.runner"/> diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index c5b5b91d570b..c07f0eb11510 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -14,99 +14,103 @@ * limitations under the License. */ +@file:JvmName("CommonAssertions") package com.android.wm.shell.flicker import android.graphics.Region import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.traces.layers.getVisibleBounds +import com.android.server.wm.traces.common.FlickerComponentName -fun FlickerTestParameter.appPairsDividerIsVisible() { +fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() { assertLayersEnd { - this.isVisible(APP_PAIR_SPLIT_DIVIDER) + this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsDividerIsInvisible() { +fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() { assertLayersEnd { - this.notContains(APP_PAIR_SPLIT_DIVIDER) + this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } } fun FlickerTestParameter.appPairsDividerBecomesVisible() { assertLayers { - this.isInvisible(DOCKED_STACK_DIVIDER) + this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() - .isVisible(DOCKED_STACK_DIVIDER) + .isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.dockedStackDividerIsVisible() { +fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() { assertLayersEnd { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } fun FlickerTestParameter.dockedStackDividerBecomesVisible() { assertLayers { - this.isInvisible(DOCKED_STACK_DIVIDER) + this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() - .isVisible(DOCKED_STACK_DIVIDER) + .isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } fun FlickerTestParameter.dockedStackDividerBecomesInvisible() { assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() - .isInvisible(DOCKED_STACK_DIVIDER) + .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.dockedStackDividerIsInvisible() { +fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() { assertLayersEnd { - this.notContains(DOCKED_STACK_DIVIDER) + this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsPrimaryBoundsIsVisible(rotation: Int, primaryLayerName: String) { +fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd( + rotation: Int, + primaryComponent: FlickerComponentName +) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(primaryLayerName) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryComponent) .coversExactly(getPrimaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisible( +fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd( rotation: Int, - primaryLayerName: String + primaryComponent: FlickerComponentName ) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) - visibleRegion(primaryLayerName) + val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryComponent) .coversExactly(getPrimaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.appPairsSecondaryBoundsIsVisible( +fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd( rotation: Int, - secondaryLayerName: String + secondaryComponent: FlickerComponentName ) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(secondaryLayerName) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(secondaryComponent) .coversExactly(getSecondaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisible( +fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd( rotation: Int, - secondaryLayerName: String + secondaryComponent: FlickerComponentName ) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) - visibleRegion(secondaryLayerName) + val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(secondaryComponent) .coversExactly(getSecondaryRegion(dividerRegion, rotation)) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt index 03b93c74233c..40891f36a5da 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -14,9 +14,11 @@ * limitations under the License. */ +@file:JvmName("CommonConstants") package com.android.wm.shell.flicker -const val IME_WINDOW_NAME = "InputMethod" +import com.android.server.wm.traces.common.FlickerComponentName + const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" -const val APP_PAIR_SPLIT_DIVIDER = "AppPairSplitDivider" -const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
\ No newline at end of file +val APP_PAIR_SPLIT_DIVIDER_COMPONENT = FlickerComponentName("", "AppPairSplitDivider#") +val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#")
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt index a6d67355f271..b63d9fffdb61 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:JvmName("WaitUtils") package com.android.wm.shell.flicker import android.os.SystemClock diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt index ef9f7421fd60..038be9c190c2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -25,7 +24,7 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.appPairsDividerIsInvisible +import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow @@ -61,7 +60,7 @@ class AppPairsTestCannotPairNonResizeableApps( // TODO pair apps through normal UX flow executeShellCommand( composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) } } } @@ -85,15 +84,13 @@ class AppPairsTestCannotPairNonResizeableApps( @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @FlakyTest + @Presubmit @Test - override fun navBarLayerIsAlwaysVisible() { - super.navBarLayerIsAlwaysVisible() - } + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() @Presubmit @Test - fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible() + fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() @Presubmit @Test @@ -103,8 +100,8 @@ class AppPairsTestCannotPairNonResizeableApps( "Non resizeable app not initialized" } testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) - isInvisible(primaryApp.defaultWindowName) + isAppWindowVisible(nonResizeableApp.component) + isAppWindowInvisible(primaryApp.component) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt index db63c4c43523..bbc6b2dbece8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -25,10 +24,10 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER -import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -54,10 +53,14 @@ class AppPairsTestPairPrimaryAndSecondaryApps( // TODO pair apps through normal UX flow executeShellCommand( composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + waitAppsShown(primaryApp, secondaryApp) } } + @Presubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + @FlakyTest @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @@ -68,14 +71,14 @@ class AppPairsTestPairPrimaryAndSecondaryApps( @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(primaryApp.defaultWindowName) - isVisible(secondaryApp.defaultWindowName) + isAppWindowVisible(primaryApp.component) + isAppWindowVisible(secondaryApp.component) } } @@ -83,10 +86,10 @@ class AppPairsTestPairPrimaryAndSecondaryApps( @Test fun appsEndingBounds() { testSpec.assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(primaryApp.defaultWindowName) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryApp.component) .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion)) - visibleRegion(secondaryApp.defaultWindowName) + visibleRegion(secondaryApp.component) .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion)) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt index c8d34237231c..bb784a809b7e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -25,7 +24,7 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow @@ -61,7 +60,7 @@ class AppPairsTestSupportPairNonResizeableApps( // TODO pair apps through normal UX flow executeShellCommand( composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) } } } @@ -77,6 +76,10 @@ class AppPairsTestSupportPairNonResizeableApps( resetMultiWindowConfig(instrumentation) } + @Presubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + @FlakyTest @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @@ -87,7 +90,7 @@ class AppPairsTestSupportPairNonResizeableApps( @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() @Presubmit @Test @@ -97,8 +100,8 @@ class AppPairsTestSupportPairNonResizeableApps( "Non resizeable app not initialized" } testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) - isVisible(primaryApp.defaultWindowName) + isAppWindowVisible(nonResizeableApp.component) + isAppWindowVisible(primaryApp.component) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt index 83df83600d11..a1a4db112dfd 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt @@ -25,10 +25,10 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER -import com.android.wm.shell.flicker.appPairsDividerIsInvisible +import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -51,9 +51,11 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( get() = { super.transition(this, it) setup { - executeShellCommand( - composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + eachRun { + executeShellCommand( + composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) + waitAppsShown(primaryApp, secondaryApp) + } } transitions { // TODO pair apps through normal UX flow @@ -73,14 +75,14 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Presubmit @Test - fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible() + fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() @Presubmit @Test fun bothAppWindowsInvisible() { testSpec.assertWmEnd { - isInvisible(primaryApp.defaultWindowName) - isInvisible(secondaryApp.defaultWindowName) + isAppWindowInvisible(primaryApp.component) + isAppWindowInvisible(secondaryApp.component) } } @@ -88,10 +90,10 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Test fun appsStartingBounds() { testSpec.assertLayersStart { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(primaryApp.defaultWindowName) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryApp.component) .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion)) - visibleRegion(secondaryApp.defaultWindowName) + visibleRegion(secondaryApp.component) .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion)) } } @@ -100,16 +102,14 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Test fun appsEndingBounds() { testSpec.assertLayersEnd { - notContains(primaryApp.defaultWindowName) - notContains(secondaryApp.defaultWindowName) + notContains(primaryApp.component) + notContains(secondaryApp.component) } } - @FlakyTest + @Presubmit @Test - override fun navBarLayerIsAlwaysVisible() { - super.navBarLayerIsAlwaysVisible() - } + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt index 1935bb97849c..9e20bbbc1a1b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt @@ -20,24 +20,23 @@ import android.app.Instrumentation import android.content.Context import android.platform.test.annotations.Presubmit import android.system.helpers.ActivityHelper -import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.isRotated import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.BaseAppHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow @@ -55,7 +54,7 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) protected val activityHelper = ActivityHelper.getInstance() protected val appPairsHelper = AppPairsHelper(instrumentation, Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT) + Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation) protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) @@ -154,39 +153,33 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) @FlakyTest(bugId = 186510496) @Test - open fun navBarLayerIsAlwaysVisible() { - testSpec.navBarLayerIsAlwaysVisible() + open fun navBarLayerIsVisible() { + testSpec.navBarLayerIsVisible() } @Presubmit @Test - open fun statusBarLayerIsAlwaysVisible() { - testSpec.statusBarLayerIsAlwaysVisible() + open fun statusBarLayerIsVisible() { + testSpec.statusBarLayerIsVisible() } @Presubmit @Test - open fun navBarWindowIsAlwaysVisible() { - testSpec.navBarWindowIsAlwaysVisible() + open fun navBarWindowIsVisible() { + testSpec.navBarWindowIsVisible() } @Presubmit @Test - open fun statusBarWindowIsAlwaysVisible() { - testSpec.statusBarWindowIsAlwaysVisible() + open fun statusBarWindowIsVisible() { + testSpec.statusBarWindowIsVisible() } @Presubmit @Test - open fun navBarLayerRotatesAndScales() { - testSpec.navBarLayerRotatesAndScales(Surface.ROTATION_0, - testSpec.config.endRotation) - } + open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Presubmit @Test - open fun statusBarLayerRotatesScales() { - testSpec.statusBarLayerRotatesScales(Surface.ROTATION_0, - testSpec.config.endRotation) - } + open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt index c875c0006703..56a2531a3fe1 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest @@ -28,10 +27,10 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.setRotation -import com.android.wm.shell.flicker.appPairsDividerIsVisible -import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible -import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -57,41 +56,43 @@ class RotateTwoLaunchedAppsInAppPairsMode( transitions { executeShellCommand(composePairsCommand( primaryTaskId, secondaryTaskId, true /* pair */)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + waitAppsShown(primaryApp, secondaryApp) setRotation(testSpec.config.endRotation) } } - @FlakyTest + @Presubmit @Test - override fun statusBarLayerIsAlwaysVisible() { - super.statusBarLayerIsAlwaysVisible() - } + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + @Presubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @Presubmit @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(primaryApp.defaultWindowName) - .isVisible(secondaryApp.defaultWindowName) + isAppWindowVisible(primaryApp.component) + isAppWindowVisible(secondaryApp.component) } } @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - @FlakyTest(bugId = 172776659) + @Presubmit @Test - fun appPairsPrimaryBoundsIsVisible() = - testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation, - primaryApp.defaultWindowName) + fun appPairsPrimaryBoundsIsVisibleAtEnd() = + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + primaryApp.component) - @FlakyTest(bugId = 172776659) + @FlakyTest @Test - fun appPairsSecondaryBoundsIsVisible() = - testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation, - secondaryApp.defaultWindowName) + fun appPairsSecondaryBoundsIsVisibleAtEnd() = + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + secondaryApp.component) companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt index c3360ca0f7d3..0699a4fd0512 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest @@ -28,12 +27,10 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.appPairsDividerIsVisible -import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible -import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -60,48 +57,50 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( this.setRotation(testSpec.config.endRotation) executeShellCommand( composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + waitAppsShown(primaryApp, secondaryApp) } } @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() @Presubmit @Test - override fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() @Presubmit @Test - override fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @FlakyTest + @Presubmit @Test - override fun statusBarLayerIsAlwaysVisible() { - super.statusBarLayerIsAlwaysVisible() - } + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + @Presubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @Presubmit @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(primaryApp.defaultWindowName) - isVisible(secondaryApp.defaultWindowName) + isAppWindowVisible(primaryApp.component) + isAppWindowVisible(secondaryApp.component) } } @FlakyTest(bugId = 172776659) @Test - fun appPairsPrimaryBoundsIsVisible() = - testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation, - primaryApp.defaultWindowName) + fun appPairsPrimaryBoundsIsVisibleAtEnd() = + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + primaryApp.component) @FlakyTest(bugId = 172776659) @Test - fun appPairsSecondaryBoundsIsVisible() = - testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation, - secondaryApp.defaultWindowName) + fun appPairsSecondaryBoundsIsVisibleAtEnd() = + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + secondaryApp.component) companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt index 512fd9a58ea8..b95193a17265 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt @@ -22,7 +22,10 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.Assume.assumeFalse +import org.junit.Before import org.junit.Test abstract class RotateTwoLaunchedAppsTransition( @@ -37,8 +40,8 @@ abstract class RotateTwoLaunchedAppsTransition( test { device.wakeUpAndGoToHomeScreen() this.setRotation(Surface.ROTATION_0) - primaryApp.launchViaIntent() - secondaryApp.launchViaIntent() + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) updateTasksId() } } @@ -52,10 +55,17 @@ abstract class RotateTwoLaunchedAppsTransition( } } + @Before + override fun setup() { + // AppPairs hasn't been updated to Shell Transition. There will be conflict on rotation. + assumeFalse(isShellTransitionsEnabled()) + super.setup() + } + @FlakyTest @Test - override fun navBarLayerIsAlwaysVisible() { - super.navBarLayerIsAlwaysVisible() + override fun navBarLayerIsVisible() { + super.navBarLayerIsVisible() } @FlakyTest diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt new file mode 100644 index 000000000000..322d8b5e4dac --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.app.INotificationManager +import android.app.Instrumentation +import android.app.NotificationManager +import android.content.Context +import android.os.ServiceManager +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerBuilderProvider +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE +import com.android.server.wm.flicker.repetitions +import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper +import org.junit.Test +import org.junit.runners.Parameterized + +/** + * Base configurations for Bubble flicker tests + */ +abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { + + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + protected val context: Context = instrumentation.context + protected val testApp = LaunchBubbleHelper(instrumentation) + + protected val notifyManager = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)) + + protected val packageManager = context.getPackageManager() + protected val uid = packageManager.getApplicationInfo( + testApp.component.packageName, 0).uid + + protected lateinit var addBubbleBtn: UiObject2 + protected lateinit var cancelAllBtn: UiObject2 + + protected abstract val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + + @JvmOverloads + protected open fun buildTransition( + extraSpec: FlickerBuilder.(Map<String, Any?>) -> Unit = {} + ): FlickerBuilder.(Map<String, Any?>) -> Unit { + return { configuration -> + + setup { + test { + notifyManager.setBubblesAllowed(testApp.component.packageName, + uid, NotificationManager.BUBBLE_PREFERENCE_ALL) + testApp.launchViaIntent(wmHelper) + addBubbleBtn = device.wait(Until.findObject( + By.text("Add Bubble")), FIND_OBJECT_TIMEOUT) + cancelAllBtn = device.wait(Until.findObject( + By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT) + } + } + + teardown { + notifyManager.setBubblesAllowed(testApp.component.packageName, + uid, NotificationManager.BUBBLE_PREFERENCE_NONE) + testApp.exit() + } + + extraSpec(this, configuration) + } + } + + @FlakyTest + @Test + fun testAppIsAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } + + @FlickerBuilderProvider + fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + repeat { testSpec.config.repetitions } + transition(this, testSpec.config) + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) + } + + const val FIND_OBJECT_TIMEOUT = 2000L + const val SYSTEM_UI_PACKAGE = SYSTEMUI_PACKAGE + const val BUBBLE_RES_NAME = "bubble_view" + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt new file mode 100644 index 000000000000..bfdcb363a818 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.content.Context +import android.graphics.Point +import android.util.DisplayMetrics +import android.view.WindowManager +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:DismissBubbleScreen` + * + * Actions: + * Dismiss a bubble notification + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 +class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val displaySize = DisplayMetrics() + + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition() { + setup { + eachRun { + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found") + } + } + transitions { + wm?.run { wm.getDefaultDisplay().getMetrics(displaySize) } ?: error("WM not found") + val dist = Point((displaySize.widthPixels / 2), displaySize.heightPixels) + val showBubble = device.wait(Until.findObject( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found") + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt new file mode 100644 index 000000000000..42eeadf3ddd9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen` + * + * Actions: + * Launch an app and enable app's bubble notification + * Send a bubble notification + * The activity for the bubble is launched + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 +class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition() { + setup { + test { + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found") + } + } + transitions { + val showBubble = device.wait(Until.findObject( + By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT) + showBubble?.run { showBubble.click() } ?: error("Bubble notify not found") + device.pressBack() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt index cf84a2c696d0..47e8c0c047a8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2021 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. @@ -14,47 +14,35 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.pip +package com.android.wm.shell.flicker.bubble -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipCloseWithDismissButton` + * Test creating a bubble notification + * + * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen` + * + * Actions: + * Launch an app and enable app's bubble notification + * Send a bubble notification */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class PipCloseWithDismissButtonTest(testSpec: FlickerTestParameter) : PipCloseTransition(testSpec) { +@Group4 +class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { - super.transition(this, it) + get() = buildTransition() { transitions { - pipApp.closePipWindow(wmHelper) + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found") } } - - @FlakyTest - @Test - override fun pipLayerBecomesInvisible() { - super.pipLayerBecomesInvisible() - } - - @FlakyTest - @Test - override fun pipWindowBecomesInvisible() { - super.pipWindowBecomesInvisible() - } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt new file mode 100644 index 000000000000..194e28fd6e8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.bubble + +import android.os.SystemClock +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:MultiBubblesScreen` + * + * Actions: + * Switch in different bubble notifications + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 +class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition() { + setup { + test { + for (i in 1..3) { + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found") + } + val showBubble = device.wait(Until.findObject( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + showBubble?.run { showBubble.click() } ?: error("Show bubble not found") + SystemClock.sleep(1000) + } + } + transitions { + val bubbles = device.wait(Until.findObjects( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + for (entry in bubbles) { + entry?.run { entry.click() } ?: error("Bubble not found") + SystemClock.sleep(1000) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 5b8cfb81016a..623055f659b9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -17,14 +17,15 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.graphics.Region +import com.android.server.wm.flicker.Flicker import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.traces.common.FlickerComponentName class AppPairsHelper( instrumentation: Instrumentation, activityLabel: String, - component: ComponentName + component: FlickerComponentName ) : BaseAppHelper(instrumentation, activityLabel, component) { fun getPrimaryBounds(dividerBounds: Region): android.graphics.Region { val primaryAppBounds = Region(0, 0, dividerBounds.bounds.right, @@ -43,5 +44,17 @@ class AppPairsHelper( companion object { const val TEST_REPETITIONS = 1 const val TIMEOUT_MS = 3_000L + + fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) { + wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump -> + val primaryAppVisible = app1?.let { + dump.wmState.isWindowSurfaceShown(app1.defaultWindowName) + } ?: false + val secondaryAppVisible = app2?.let { + dump.wmState.isWindowSurfaceShown(app2.defaultWindowName) + } ?: false + primaryAppVisible && secondaryAppVisible + } + } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt index 4fe69ad7fabe..57bc0d580d72 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -17,9 +17,9 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.content.pm.PackageManager.FEATURE_LEANBACK import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY +import android.os.SystemProperties import android.support.test.launcherhelper.LauncherStrategyFactory import android.util.Log import androidx.test.uiautomator.By @@ -27,13 +27,13 @@ import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.flicker.helpers.StandardAppHelper -import com.android.server.wm.traces.parser.toWindowName +import com.android.server.wm.traces.common.FlickerComponentName import java.io.IOException abstract class BaseAppHelper( instrumentation: Instrumentation, launcherName: String, - component: ComponentName + component: FlickerComponentName ) : StandardAppHelper( instrumentation, launcherName, @@ -60,6 +60,9 @@ abstract class BaseAppHelper( companion object { private const val APP_CLOSE_WAIT_TIME_MS = 3_000L + fun isShellTransitionsEnabled() = + SystemProperties.getBoolean("persist.debug.shell_transit", false) + fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { try { SystemUtil.runShellCommand(instrumentation, cmd) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt index b4ae18749b34..471e010cf560 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt @@ -17,10 +17,11 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.testapp.Components class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.FixedActivity.LABEL, - Components.FixedActivity.COMPONENT + Components.FixedActivity.COMPONENT.toFlickerComponent() )
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index cac46fe676b3..0f00edea136f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -21,13 +21,14 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.testapp.Components open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.ImeActivity.LABEL, - Components.ImeActivity.COMPONENT + Components.ImeActivity.COMPONENT.toFlickerComponent() ) { /** * Opens the IME and wait for it to be displayed @@ -61,7 +62,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( if (wmHelper == null) { device.waitForIdle() } else { - require(wmHelper.waitImeWindowShown()) { "IME did not appear" } + require(wmHelper.waitImeShown()) { "IME did not appear" } } } @@ -78,7 +79,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( if (wmHelper == null) { uiDevice.waitForIdle() } else { - require(wmHelper.waitImeWindowGone()) { "IME did did not close" } + require(wmHelper.waitImeGone()) { "IME did did not close" } } } else { // While pressing the back button should close the IME on TV as well, it may also lead diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt new file mode 100644 index 000000000000..6695c17ed514 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.helpers + +import android.app.Instrumentation +import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.wm.shell.flicker.testapp.Components + +class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper( + instrumentation, + Components.LaunchBubbleActivity.LABEL, + Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent() +) { + + companion object { + const val TEST_REPETITIONS = 1 + const val TIMEOUT_MS = 3_000L + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt index 7f99e62b36b0..12ccbafce651 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt @@ -17,14 +17,14 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.content.Context import android.provider.Settings +import com.android.server.wm.traces.common.FlickerComponentName class MultiWindowHelper( instrumentation: Instrumentation, activityLabel: String, - componentsInfo: ComponentName + componentsInfo: FlickerComponentName ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index f4dd7decb1b7..2357b0debb33 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -17,12 +17,16 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import android.graphics.Rect import android.media.session.MediaController import android.media.session.MediaSessionManager import android.os.SystemClock import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild @@ -31,7 +35,7 @@ import com.android.wm.shell.flicker.testapp.Components class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.PipActivity.LABEL, - Components.PipActivity.COMPONENT + Components.PipActivity.COMPONENT.toFlickerComponent() ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) @@ -62,7 +66,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( stringExtras: Map<String, String> ) { super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) - wmHelper.waitFor { it.wmState.hasPipWindow() } + wmHelper.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } } private fun focusOnObject(selector: BySelector): Boolean { @@ -84,7 +88,11 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( clickObject(ENTER_PIP_BUTTON_ID) // Wait on WMHelper or simply wait for 3 seconds - wmHelper?.waitFor { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000) + wmHelper?.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000) + // when entering pip, the dismiss button is visible at the start. to ensure the pip + // animation is complete, wait until the pip dismiss button is no longer visible. + // b/176822698: dismiss-only state will be removed in the future + uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) } fun clickStartMediaSessionButton() { @@ -113,61 +121,61 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } } + private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { + val windowRegion = wmHelper.getWindowRegion(component) + require(!windowRegion.isEmpty) { + "Unable to find a PIP window in the current state" + } + return windowRegion.bounds + } + /** - * Expands the pip window and dismisses it by clicking on the X button. - * - * Note, currently the View coordinates reported by the accessibility are relative to - * the window, so the correct coordinates need to be calculated - * - * For example, in a PIP window located at Rect(508, 1444 - 1036, 1741), the - * dismiss button coordinates are shown as Rect(650, 0 - 782, 132), with center in - * Point(716, 66), instead of Point(970, 1403) - * - * See b/179337864 + * Taps the pip window and dismisses it by clicking on the X button. */ fun closePipWindow(wmHelper: WindowManagerStateHelper) { if (isTelevision) { uiDevice.closeTvPipWindow() } else { - expandPipWindow(wmHelper) - val exitPipObject = uiDevice.findObject(By.res(SYSTEMUI_PACKAGE, "dismiss")) - requireNotNull(exitPipObject) { "PIP window dismiss button not found" } - val dismissButtonBounds = exitPipObject.visibleBounds + val windowRect = getWindowRect(wmHelper) + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + // search and interact with the dismiss button + val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") + uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) + val dismissPipObject = uiDevice.findObject(dismissSelector) + ?: error("PIP window dismiss button not found") + val dismissButtonBounds = dismissPipObject.visibleBounds uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) } // Wait for animation to complete. - wmHelper.waitFor { !it.wmState.hasPipWindow() } + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } wmHelper.waitForHomeActivityVisible() } /** - * Click once on the PIP window to expand it + * Close the pip window by pressing the expand button */ - fun expandPipWindow(wmHelper: WindowManagerStateHelper) { - val windowRegion = wmHelper.getWindowRegion(component) - require(!windowRegion.isEmpty) { - "Unable to find a PIP window in the current state" - } - val windowRect = windowRegion.bounds + fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { + val windowRect = getWindowRect(wmHelper) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // Ensure WindowManagerService wait until all animations have completed + // search and interact with the expand button + val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") + uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) + val expandPipObject = uiDevice.findObject(expandSelector) + ?: error("PIP window expand button not found") + val expandButtonBounds = expandPipObject.visibleBounds + uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() - mInstrumentation.uiAutomation.syncInputTransactions() } /** - * Double click on the PIP window to reopen to app + * Double click on the PIP window to expand it */ - fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { - val windowRegion = wmHelper.getWindowRegion(component) - require(!windowRegion.isEmpty) { - "Unable to find a PIP window in the current state" - } - val windowRect = windowRegion.bounds + fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) { + val windowRect = getWindowRect(wmHelper) uiDevice.click(windowRect.centerX(), windowRect.centerY()) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - wmHelper.waitFor { !it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt index ba13e38ae9e3..4d0fbc4a0e38 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt @@ -17,10 +17,11 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.testapp.Components class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.SimpleActivity.LABEL, - Components.SimpleActivity.COMPONENT + Components.SimpleActivity.COMPONENT.toFlickerComponent() )
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt index 901b7a393291..0ec9b2d869a8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt @@ -17,32 +17,39 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName +import android.content.res.Resources +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.testapp.Components class SplitScreenHelper( instrumentation: Instrumentation, activityLabel: String, - componentsInfo: ComponentName + componentsInfo: FlickerComponentName ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { companion object { const val TEST_REPETITIONS = 1 const val TIMEOUT_MS = 3_000L + // TODO: remove all legacy split screen flicker tests when legacy split screen is fully + // deprecated. + fun isUsingLegacySplit(): Boolean = + Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useLegacySplit) + fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper = SplitScreenHelper(instrumentation, Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT) + Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper = SplitScreenHelper(instrumentation, Components.SplitScreenSecondaryActivity.LABEL, - Components.SplitScreenSecondaryActivity.COMPONENT) + Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()) fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper = SplitScreenHelper(instrumentation, Components.NonResizeableActivity.LABEL, - Components.NonResizeableActivity.COMPONENT) + Components.NonResizeableActivity.COMPONENT.toFlickerComponent()) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt index 4f12f2bb9f5f..bd44d082a1aa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt @@ -18,20 +18,21 @@ package com.android.wm.shell.flicker.legacysplitscreen import android.platform.test.annotations.Presubmit import android.view.Surface +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.HOME_WINDOW_TITLE -import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -48,7 +49,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 +@Group4 class EnterSplitScreenDockActivity( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -60,16 +61,16 @@ class EnterSplitScreenDockActivity( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, LIVE_WALLPAPER_PACKAGE_NAME, - splitScreenApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, *HOME_WINDOW_TITLE) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT, + splitScreenApp.component, FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, LAUNCHER_COMPONENT) @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test @@ -77,27 +78,39 @@ class EnterSplitScreenDockActivity( @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) } } + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 + supportedRotations = listOf(Surface.ROTATION_0), // bugId = 179116910 + supportedNavigationModes = listOf( + WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt index f91f634a00e5..625d48b8ab5a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt @@ -22,10 +22,11 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -42,6 +43,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 class EnterSplitScreenFromDetachedRecentTask( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -61,24 +63,34 @@ class EnterSplitScreenFromDetachedRecentTask( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, - splitScreenApp.defaultWindowName) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, + splitScreenApp.component) @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt index 85ded8a45233..2ed2806af528 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt @@ -22,18 +22,17 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -49,7 +48,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 +@Group4 class EnterSplitScreenLaunchToSide( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -62,22 +61,22 @@ class EnterSplitScreenLaunchToSide( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - secondaryApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, + secondaryApp.component, FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test - fun dockedStackSecondaryBoundsIsVisible() = - testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation, - secondaryApp.defaultWindowName) + fun dockedStackSecondaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + secondaryApp.component) @Presubmit @Test @@ -85,15 +84,35 @@ class EnterSplitScreenLaunchToSide( @Presubmit @Test - fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + // when the app is launched, first the activity becomes visible, then the + // SnapshotStartingWindow appears and then the app window becomes visible. + // Because we log WM once per frame, sometimes the activity and the window + // become visible in the same entry, sometimes not, thus it is not possible to + // assert the visibility of the activity here + this.isAppWindowInvisible(secondaryApp.component) + .then() + // during re-parenting, the window may disappear and reappear from the + // trace, this occurs because we log only 1x per frame + .notContains(secondaryApp.component, isOptional = true) + .then() + .isAppWindowVisible(secondaryApp.component) + } + } + + @Presubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt index e958bf39930e..ee6cf341c9ff 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt @@ -22,11 +22,11 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.canSplitScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -50,7 +50,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group1 +@Group4 class EnterSplitScreenNotSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -70,12 +70,12 @@ class EnterSplitScreenNotSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, - nonResizeableApp.defaultWindowName, - splitScreenApp.defaultWindowName) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, + nonResizeableApp.component, + splitScreenApp.component) @Before override fun setup() { @@ -91,7 +91,12 @@ class EnterSplitScreenNotSupportNonResizable( @Presubmit @Test - fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt index d3acc82121b0..163b6ffda6e2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt @@ -25,8 +25,8 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -67,12 +67,12 @@ class EnterSplitScreenSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, - nonResizeableApp.defaultWindowName, - splitScreenApp.defaultWindowName) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, + nonResizeableApp.component, + splitScreenApp.component) @Before override fun setup() { @@ -88,16 +88,21 @@ class EnterSplitScreenSupportNonResizable( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt index bad46836dcb7..2b629b0a7eb5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.Postsubmit import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -24,15 +24,13 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -67,31 +65,52 @@ class ExitLegacySplitScreenFromBottom( } } transitions { - device.exitSplitScreenFromBottom() + device.exitSplitScreenFromBottom(wmHelper) } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, + splitScreenApp.component, secondaryApp.component, + FlickerComponentName.SNAPSHOT) - @Presubmit + @Postsubmit @Test - fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(DOCKED_STACK_DIVIDER) + fun layerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) + .then() + .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) + } + } @FlakyTest @Test - fun appWindowBecomesInVisible() = - testSpec.appWindowBecomesInVisible(secondaryApp.defaultWindowName) + fun appWindowBecomesInVisible() { + testSpec.assertWm { + this.isAppWindowVisible(secondaryApp.component) + .then() + .isAppWindowInvisible(secondaryApp.component) + } + } + + @Postsubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - @Presubmit + @Postsubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - @Presubmit + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @FlakyTest @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt index 76dcd8b89242..95fe3bef4852 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt @@ -24,15 +24,13 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -71,31 +69,52 @@ class ExitPrimarySplitScreenShowSecondaryFullscreen( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, + splitScreenApp.component, secondaryApp.component, + FlickerComponentName.SNAPSHOT) - @FlakyTest(bugId = 175687842) + @Presubmit @Test - fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @FlakyTest @Test - fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName) + fun layerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(splitScreenApp.component) + .then() + .isInvisible(splitScreenApp.component) + } + } @FlakyTest @Test - fun appWindowBecomesInVisible() = - testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + fun appWindowBecomesInVisible() { + testSpec.assertWm { + this.isAppWindowVisible(splitScreenApp.component) + .then() + .isAppWindowInvisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() + + @Presubmit + @Test + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt index d0a64b3774c7..f7d628d48769 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt @@ -23,15 +23,11 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -72,11 +68,11 @@ class LegacySplitScreenFromIntentNotSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, - nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + nonResizeableApp.component, splitScreenApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -92,44 +88,109 @@ class LegacySplitScreenFromIntentNotSupportNonResizable( @Presubmit @Test - fun resizableAppLayerBecomesInvisible() = - testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName) + fun resizableAppLayerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(splitScreenApp.component) + .then() + .isInvisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.notContains(nonResizeableApp.component) + .then() + .isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } + /** + * Assets that [splitScreenApp] exists at the start of the trace and, once it becomes + * invisible, it remains invisible until the end of the trace. + */ @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun resizableAppWindowBecomesInvisible() { + testSpec.assertWm { + // when the activity gets PAUSED the window may still be marked as visible + // it will be updated in the next log entry. This occurs because we record 1x + // per frame, thus ignore activity check here + this.isAppWindowVisible(splitScreenApp.component) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowInvisible(splitScreenApp.component) + } + } + /** + * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then + * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes + * visible, it remains visible until the end of the trace. + */ @Presubmit @Test - fun resizableAppWindowBecomesInvisible() = - testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + this.notContains(nonResizeableApp.component) + .then() + // we log once per frame, upon logging, window may be visible or not depending + // on what was processed until that moment. Both behaviors are correct + .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowVisible(nonResizeableApp.component) + } + } + /** + * Asserts that both the app window and the activity are visible at the end of the trace + */ @Presubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisibleAtEnd() { + testSpec.assertWmEnd { + isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @Presubmit @Test fun onlyNonResizableAppWindowIsVisibleAtEnd() { testSpec.assertWmEnd { - isInvisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowInvisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt index c26c05fa8db6..a5c6571f68de 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt @@ -23,13 +23,11 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -70,11 +68,11 @@ class LegacySplitScreenFromIntentSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, - nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + nonResizeableApp.component, splitScreenApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -90,27 +88,59 @@ class LegacySplitScreenFromIntentSupportNonResizable( @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } + /** + * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then + * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes + * visible, it remains visible until the end of the trace. + */ @Presubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + this.notContains(nonResizeableApp.component) + .then() + // we log once per frame, upon logging, window may be visible or not depending + // on what was processed until that moment. Both behaviors are correct + .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppsWindowsAreVisibleAtEnd() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt index fb1758975442..6f486b0ddfea 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,16 +24,12 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -73,11 +70,11 @@ class LegacySplitScreenFromRecentNotSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME, - splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -93,37 +90,73 @@ class LegacySplitScreenFromRecentNotSupportNonResizable( @Presubmit @Test - fun resizableAppLayerBecomesInvisible() = - testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName) + fun resizableAppLayerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(splitScreenApp.component) + .then() + .isInvisible(splitScreenApp.component) + } + } @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun resizableAppWindowBecomesInvisible() = - testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + fun resizableAppWindowBecomesInvisible() { + testSpec.assertWm { + // when the activity gets PAUSED the window may still be marked as visible + // it will be updated in the next log entry. This occurs because we record 1x + // per frame, thus ignore activity check here + this.isAppWindowVisible(splitScreenApp.component) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowInvisible(splitScreenApp.component) + } + } - @Presubmit + @Postsubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(nonResizeableApp.component) + .then() + .isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @Presubmit @Test fun onlyNonResizableAppWindowIsVisibleAtEnd() { testSpec.assertWmEnd { - isInvisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowInvisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt index a9c28efcdf44..f03c927b8d58 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt @@ -23,14 +23,12 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -71,11 +69,11 @@ class LegacySplitScreenFromRecentSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME, - splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -91,27 +89,60 @@ class LegacySplitScreenFromRecentSupportNonResizable( @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + // when the app is launched, first the activity becomes visible, then the + // SnapshotStartingWindow appears and then the app window becomes visible. + // Because we log WM once per frame, sometimes the activity and the window + // become visible in the same entry, sometimes not, thus it is not possible to + // assert the visibility of the activity here + this.isAppWindowInvisible(nonResizeableApp.component) + .then() + // during re-parenting, the window may disappear and reappear from the + // trace, this occurs because we log only 1x per frame + .notContains(nonResizeableApp.component, isOptional = true) + .then() + // if the window reappears after re-parenting it will most likely not + // be visible in the first log entry (because we log only 1x per frame) + .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) + .then() + .isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppsWindowsAreVisibleAtEnd() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt index a4d2ab51e358..2ccd03bf1d6a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt @@ -16,10 +16,9 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.support.test.launcherhelper.LauncherStrategyFactory import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -27,21 +26,19 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation -import com.android.server.wm.flicker.focusDoesNotChange +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.exitSplitScreen import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible import com.android.wm.shell.flicker.helpers.SimpleAppHelper import org.junit.FixMethodOrder @@ -62,8 +59,6 @@ import org.junit.runners.Parameterized class LegacySplitScreenToLauncher( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - private val launcherPackageName = LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage private val testApp = SimpleAppHelper(instrumentation) override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit @@ -90,51 +85,69 @@ class LegacySplitScreenToLauncher( } } - override val ignoredWindows: List<String> - get() = listOf(launcherPackageName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible() + fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() @Presubmit @Test - fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation) + fun entireScreenCovered() = testSpec.entireScreenCovered() @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Presubmit @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - @Presubmit + @Postsubmit @Test fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible() + @Postsubmit + @Test + fun layerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + .then() + .isInvisible(testApp.component) + } + } + + @Postsubmit + @Test + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } + @Presubmit @Test - fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(testApp.getPackage()) + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() - @FlakyTest(bugId = 151179149) + @Presubmit @Test - fun focusDoesNotChange() = testSpec.focusDoesNotChange() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt index e8d4d1e9ada2..661c8b69068e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt @@ -31,11 +31,14 @@ import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.After +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test @@ -46,12 +49,17 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) - protected val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage + protected val LAUNCHER_COMPONENT = FlickerComponentName("", + LauncherStrategyFactory.getInstance(instrumentation) + .launcherStrategy.supportedLauncherPackage) private var prevDevEnableNonResizableMultiWindow = 0 @Before open fun setup() { + // Only run legacy split tests when the system is using legacy split screen. + assumeTrue(SplitScreenHelper.isUsingLegacySplit()) + // Legacy split is having some issue with Shell transition, and will be deprecated soon. + assumeFalse(isShellTransitionsEnabled()) prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context) if (prevDevEnableNonResizableMultiWindow != 0) { // Turn off the development option @@ -70,8 +78,9 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa * * b/182720234 */ - open val ignoredWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + open val ignoredWindows: List<FlickerComponentName> = listOf( + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { configuration -> @@ -138,9 +147,9 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa } companion object { - internal const val LIVE_WALLPAPER_PACKAGE_NAME = - "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2" - internal const val LETTERBOX_NAME = "Letterbox" - internal const val TOAST_NAME = "Toast" + internal val LIVE_WALLPAPER_COMPONENT = FlickerComponentName("", + "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2") + internal val LETTERBOX_COMPONENT = FlickerComponentName("", "Letterbox") + internal val TOAST_COMPONENT = FlickerComponentName("", "Toast") } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt index 05eb5f49a641..34eff80a04bc 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt @@ -24,15 +24,11 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarLayerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.appPairsDividerBecomesVisible import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder @@ -62,22 +58,28 @@ class OpenAppToLegacySplitScreen( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @FlakyTest @Test - fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(splitScreenApp.getPackage()) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(splitScreenApp.component) + .then() + .isAppWindowVisible(splitScreenApp.component) + } + } - @FlakyTest + @Presubmit @Test - fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation) + fun entireScreenCovered() = testSpec.entireScreenCovered() @Presubmit @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Presubmit @Test @@ -85,12 +87,27 @@ class OpenAppToLegacySplitScreen( @FlakyTest @Test - fun layerBecomesVisible() = testSpec.layerBecomesVisible(splitScreenApp.getPackage()) + fun layerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(splitScreenApp.component) + .then() + .isVisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + fun focusChanges() { + testSpec.assertEventLog { + this.focusChanges(splitScreenApp.`package`, + "recents_animation_input_consumer", "NexusLauncherActivity") + } + } - @FlakyTest(bugId = 151179149) + @Presubmit @Test - fun focusChanges() = testSpec.focusChanges(splitScreenApp.`package`, - "recents_animation_input_consumer", "NexusLauncherActivity") + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt index 3e83b6382939..58e1def6f37a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt @@ -27,24 +27,24 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.resizeSplitScreen import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT import com.android.wm.shell.flicker.helpers.SimpleAppHelper +import com.android.wm.shell.flicker.testapp.Components import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -101,16 +101,16 @@ class ResizeLegacySplitScreen( } @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @FlakyTest(bugId = 156223549) @Test fun topAppWindowIsAlwaysVisible() { testSpec.assertWm { - this.showsAppWindow(sSimpleActivity) + this.isAppWindowVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent()) } } @@ -118,45 +118,43 @@ class ResizeLegacySplitScreen( @Test fun bottomAppWindowIsAlwaysVisible() { testSpec.assertWm { - this.showsAppWindow(sImeActivity) + this.isAppWindowVisible(Components.ImeActivity.COMPONENT.toFlickerComponent()) } } @Test - fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible() + fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Test - fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation) + fun entireScreenCovered() = testSpec.entireScreenCovered() @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Test fun topAppLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(sSimpleActivity) + this.isVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent()) } } @Test fun bottomAppLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(sImeActivity) + this.isVisible(Components.ImeActivity.COMPONENT.toFlickerComponent()) } } @Test fun dividerLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } @@ -166,7 +164,7 @@ class ResizeLegacySplitScreen( testSpec.assertLayersStart { val displayBounds = WindowUtils.displayBounds val dividerBounds = - entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds val topAppBounds = Region(0, 0, dividerBounds.right, dividerBounds.top + WindowUtils.dockedStackDividerInset) @@ -174,8 +172,10 @@ class ResizeLegacySplitScreen( dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) - visibleRegion("SimpleActivity").coversExactly(topAppBounds) - visibleRegion("ImeActivity").coversExactly(bottomAppBounds) + visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) + .coversExactly(topAppBounds) + visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) + .coversExactly(bottomAppBounds) } } @@ -185,7 +185,7 @@ class ResizeLegacySplitScreen( testSpec.assertLayersStart { val displayBounds = WindowUtils.displayBounds val dividerBounds = - entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds val topAppBounds = Region(0, 0, dividerBounds.right, dividerBounds.top + WindowUtils.dockedStackDividerInset) @@ -194,8 +194,10 @@ class ResizeLegacySplitScreen( displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) - visibleRegion(sSimpleActivity).coversExactly(topAppBounds) - visibleRegion(sImeActivity).coversExactly(bottomAppBounds) + visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) + .coversExactly(topAppBounds) + visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) + .coversExactly(bottomAppBounds) } } @@ -207,8 +209,6 @@ class ResizeLegacySplitScreen( } companion object { - private const val sSimpleActivity = "SimpleActivity" - private const val sImeActivity = "ImeActivity" private val startRatio = Rational(1, 3) private val stopRatio = Rational(2, 3) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt index 58482eaae3f5..8a50bc0b20cf 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt @@ -24,18 +24,16 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -66,38 +64,44 @@ class RotateOneLaunchedAppAndEnterSplitScreen( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @FlakyTest @Test - fun appWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(splitScreenApp.component) + .then() + .isAppWindowVisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt index 06828d6adb26..84676a9186be 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt @@ -24,18 +24,16 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -66,35 +64,43 @@ class RotateOneLaunchedAppInSplitScreenMode( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = testSpec.dockedStackPrimaryBoundsIsVisible( - testSpec.config.startRotation, splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd( + testSpec.config.startRotation, splitScreenApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales( - testSpec.config.startRotation, testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales( - testSpec.config.startRotation, testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @FlakyTest @Test - fun appWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(splitScreenApp.component) + .then() + .isAppWindowVisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt index f8e32bf171d8..2abdca9216f9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt @@ -18,26 +18,23 @@ package com.android.wm.shell.flicker.legacysplitscreen import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -69,42 +66,63 @@ class RotateTwoLaunchedAppAndEnterSplitScreen( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test - fun dockedStackSecondaryBoundsIsVisible() = - testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation, - secondaryApp.defaultWindowName) + fun dockedStackSecondaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + secondaryApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales( - testSpec.config.startRotation, testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() + + @Presubmit + @Test + fun appWindowBecomesVisible() { + testSpec.assertWm { + // when the app is launched, first the activity becomes visible, then the + // SnapshotStartingWindow appears and then the app window becomes visible. + // Because we log WM once per frame, sometimes the activity and the window + // become visible in the same entry, sometimes not, thus it is not possible to + // assert the visibility of the activity here + this.isAppWindowInvisible(secondaryApp.component) + .then() + // during re-parenting, the window may disappear and reappear from the + // trace, this occurs because we log only 1x per frame + .notContains(secondaryApp.component, isOptional = true) + .then() + // if the window reappears after re-parenting it will most likely not + // be visible in the first log entry (because we log only 1x per frame) + .isAppWindowInvisible(secondaryApp.component, isOptional = true) + .then() + .isAppWindowVisible(secondaryApp.component) + } + } @Presubmit @Test - fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName) + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt index cb246ca0b694..fe9b9f514015 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt @@ -24,20 +24,18 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -74,44 +72,55 @@ class RotateTwoLaunchedAppInSplitScreenMode( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test - fun dockedStackSecondaryBoundsIsVisible() = - testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation, - secondaryApp.defaultWindowName) + fun dockedStackSecondaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + secondaryApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @FlakyTest @Test - fun appWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(secondaryApp.component) + .then() + .isAppWindowVisible(secondaryApp.component) + } + } + + @Presubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() + + @Presubmit + @Test + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt index 2a660747bc1d..f9b08000290f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2021 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. @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:JvmName("CommonAssertions") package com.android.wm.shell.flicker.pip -internal const val PIP_WINDOW_TITLE = "PipMenuActivity" +internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity" diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index b6af26060050..52a744f3897d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder import org.junit.FixMethodOrder @@ -32,8 +33,21 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch. + * Test entering pip from an app by interacting with the app UI + * * To run this test: `atest WMShellFlickerTests:EnterPipTest` + * + * Actions: + * Launch an app in full screen + * Press an "enter pip" button to put [pipApp] in pip mode + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @@ -41,49 +55,121 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true, stringExtras = emptyMap()) { transitions { - pipApp.clickEnterPipButton() - pipApp.expandPipWindow(wmHelper) + pipApp.clickEnterPipButton(wmHelper) } } - @FlakyTest + /** + * Checks [pipApp] window remains visible throughout the animation + */ + @Presubmit @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() + fun pipAppWindowAlwaysVisible() { + testSpec.assertWm { + this.isAppWindowVisible(pipApp.component) + } } + /** + * Checks [pipApp] layer remains visible throughout the animation + */ @Presubmit @Test - fun pipAppWindowAlwaysVisible() { + fun pipAppLayerAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(pipApp.component) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipWindowRemainInsideVisibleBounds() { testSpec.assertWm { - this.showsAppWindow(pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } - @FlakyTest + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit @Test - fun pipLayerBecomesVisible() { + fun pipLayerRemainInsideVisibleBounds() { testSpec.assertLayers { - this.isVisible(pipApp.windowName) + coversAtMost(displayBounds, pipApp.component) } } - @FlakyTest + /** + * Checks that the visible region of [pipApp] always reduces during the animation + */ + @Presubmit @Test - fun pipWindowBecomesVisible() { - testSpec.assertWm { - invoke("pipWindowIsNotVisible") { - verify("Has no pip window").that(it.wmState.hasPipWindow()).isTrue() - }.then().invoke("pipWindowIsVisible") { - verify("Has pip window").that(it.wmState.hasPipWindow()).isTrue() + fun pipLayerReduces() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.coversAtMost(previous.visibleRegion.region) } } } + /** + * Checks that [pipApp] window becomes pinned + */ + @Presubmit + @Test + fun pipWindowBecomesPinned() { + testSpec.assertWm { + invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp.component) } + .then() + .invoke("pipWindowIsPinned") { it.isPinned(pipApp.component) } + } + } + + /** + * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation + */ + @Presubmit + @Test + fun launcherLayerBecomesVisible() { + testSpec.assertLayers { + isInvisible(LAUNCHER_COMPONENT) + .then() + .isVisible(LAUNCHER_COMPONENT) + } + } + + /** + * Checks the focus doesn't change during the animation + */ + @FlakyTest + @Test + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index 3a1456e53f87..c8c3f4d64294 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -25,7 +25,11 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.helpers.FixedAppHelper import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT @@ -38,8 +42,22 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip with orientation changes. - * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + * Test entering pip while changing orientation (from app in landscape to pip window in portrait) + * + * To run this test: `atest EnterPipToOtherOrientationTest:EnterPipToOtherOrientationTest` + * + * Actions: + * Launch [testApp] on a fixed portrait orientation + * Launch [pipApp] on a fixed landscape orientation + * Broadcast action [ACTION_ENTER_PIP] to enter pip mode + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @@ -53,6 +71,9 @@ class EnterPipToOtherOrientationTest( private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { configuration -> setupAndTeardown(this, configuration) @@ -79,65 +100,125 @@ class EnterPipToOtherOrientationTest( broadcastActionTrigger.doAction(ACTION_ENTER_PIP) wmHelper.waitFor { it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() + // during rotation the status bar becomes invisible and reappears at the end + wmHelper.waitForNavBarStatusBarVisible() } } + /** + * Checks that the [FlickerComponentName.NAV_BAR] has the correct position at + * the start and end of the transition + */ @FlakyTest @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest + /** + * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at + * the start and end of the transition + */ + @Presubmit @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - @FlakyTest + /** + * Checks that all parts of the screen are covered at the start and end of the transition + * + * TODO b/197726599 Prevents all states from being checked + */ + @Presubmit @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() - } + override fun entireScreenCovered() = testSpec.entireScreenCovered(allStates = false) + /** + * Checks [pipApp] window remains visible and on top throughout the transition + */ @Presubmit @Test fun pipAppWindowIsAlwaysOnTop() { testSpec.assertWm { - showsAppWindowOnTop(pipApp.defaultWindowName) + isAppWindowOnTop(pipApp.component) } } + /** + * Checks that [testApp] window is not visible at the start + */ @Presubmit @Test - fun pipAppHidesTestApp() { + fun testAppWindowInvisibleOnStart() { testSpec.assertWmStart { - isInvisible(testApp.defaultWindowName) + isAppWindowInvisible(testApp.component) } } + /** + * Checks that [testApp] window is visible at the end + */ @Presubmit @Test - fun testAppWindowIsVisible() { + fun testAppWindowVisibleOnEnd() { testSpec.assertWmEnd { - isVisible(testApp.defaultWindowName) + isAppWindowVisible(testApp.component) + } + } + + /** + * Checks that [testApp] layer is not visible at the start + */ + @Presubmit + @Test + fun testAppLayerInvisibleOnStart() { + testSpec.assertLayersStart { + isInvisible(testApp.component) + } + } + + /** + * Checks that [testApp] layer is visible at the end + */ + @Presubmit + @Test + fun testAppLayerVisibleOnEnd() { + testSpec.assertLayersEnd { + isVisible(testApp.component) } } + /** + * Checks that the visible region of [pipApp] covers the full display area at the start of + * the transition + */ @Presubmit @Test - fun pipAppLayerHidesTestApp() { + fun pipAppLayerCoversFullScreenOnStart() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversExactly(startingBounds) - isInvisible(testApp.defaultWindowName) + visibleRegion(pipApp.component).coversExactly(startingBounds) } } + /** + * Checks that the visible region of [testApp] plus the visible region of [pipApp] + * cover the full display area at the end of the transition + */ @Presubmit @Test - fun testAppLayerCoversFullScreen() { + fun testAppPlusPipLayerCoversFullScreenOnEnd() { testSpec.assertLayersEnd { - visibleRegion(testApp.defaultWindowName).coversExactly(endingBounds) + val pipRegion = visibleRegion(pipApp.component).region + visibleRegion(testApp.component) + .plus(pipRegion) + .coversExactly(endingBounds) } } companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt new file mode 100644 index 000000000000..64b7eb53bd6f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import org.junit.Test + +/** + * Base class for pip expand tests + */ +abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + protected val testApp = FixedAppHelper(instrumentation) + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipAppWindowRemainInsideVisibleBounds() { + testSpec.assertWm { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipAppLayerRemainInsideVisibleBounds() { + testSpec.assertLayers { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks both app windows are visible at the start of the transition (with [pipApp] on top). + * Then, during the transition, [testApp] becomes invisible and [pipApp] remains visible + */ + @Presubmit + @Test + open fun showBothAppWindowsThenHidePip() { + testSpec.assertWm { + // when the activity is STOPPING, sometimes it becomes invisible in an entry before + // the window, sometimes in the same entry. This occurs because we log 1x per frame + // thus we ignore activity here + isAppWindowVisible(testApp.component) + .isAppWindowOnTop(pipApp.component) + .then() + .isAppWindowInvisible(testApp.component) + .isAppWindowVisible(pipApp.component) + } + } + + /** + * Checks both app layers are visible at the start of the transition. Then, during the + * transition, [testApp] becomes invisible and [pipApp] remains visible + */ + @Presubmit + @Test + open fun showBothAppLayersThenHidePip() { + testSpec.assertLayers { + isVisible(testApp.component) + .isVisible(pipApp.component) + .then() + .isInvisible(testApp.component) + .isVisible(pipApp.component) + } + } + + /** + * Checks that the visible region of [testApp] plus the visible region of [pipApp] + * cover the full display area at the start of the transition + */ + @Presubmit + @Test + open fun testPlusPipAppsCoverFullScreenAtStart() { + testSpec.assertLayersStart { + val pipRegion = visibleRegion(pipApp.component).region + visibleRegion(testApp.component) + .plus(pipRegion) + .coversExactly(displayBounds) + } + } + + /** + * Checks that the visible region of [pipApp] covers the full display area at the end of + * the transition + */ + @Presubmit + @Test + open fun pipAppCoversFullScreenAtEnd() { + testSpec.assertLayersEnd { + visibleRegion(pipApp.component).coversExactly(displayBounds) + } + } + + /** + * Checks that the visible region of [pipApp] always expands during the animation + */ + @Presubmit + @Test + open fun pipLayerExpands() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.coversAtLeast(previous.visibleRegion.region) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt index eae7e973711c..5207fed59208 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt @@ -20,15 +20,16 @@ import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.focusChanges import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.startRotation import org.junit.Test -import org.junit.runners.Parameterized -abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +/** + * Base class for exiting pip (closing pip window) without returning to the app + */ +abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true) { configuration -> setup { @@ -43,37 +44,49 @@ abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransitio } } + /** + * Checks that [pipApp] window is pinned and visible at the start and then becomes + * unpinned and invisible at the same moment, and remains unpinned and invisible + * until the end of the transition + */ @Presubmit @Test open fun pipWindowBecomesInvisible() { testSpec.assertWm { - this.showsAppWindow(PIP_WINDOW_TITLE) - .then() - .hidesAppWindow(PIP_WINDOW_TITLE) + this.invoke("hasPipWindow") { + it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component) + }.then().invoke("!hasPipWindow") { + it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component) + } } } + /** + * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start + * of the transition. Then [pipApp] layer becomes invisible, and remains invisible + * until the end of the transition + */ @Presubmit @Test open fun pipLayerBecomesInvisible() { testSpec.assertLayers { - this.isVisible(PIP_WINDOW_TITLE) + this.isVisible(pipApp.component) + .isVisible(LAUNCHER_COMPONENT) .then() - .isInvisible(PIP_WINDOW_TITLE) + .isInvisible(pipApp.component) + .isVisible(LAUNCHER_COMPONENT) } } + /** + * Checks that the focus changes between the [pipApp] window and the launcher when + * closing the pip window + */ @FlakyTest(bugId = 151179149) @Test - open fun focusChanges() = testSpec.focusChanges(pipApp.launcherName, "NexusLauncherActivity") - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + open fun focusChanges() { + testSpec.assertEventLog { + this.focusChanges(pipApp.launcherName, "NexusLauncherActivity") } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt new file mode 100644 index 000000000000..b53342d6f2f7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window back to full screen via the expand button + * + * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Launch another full screen mode [testApp] + * Expand [pipApp] app to full screen by clicking on the pip window and + * then on the expand button + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class ExitPipViaExpandButtonClickTest( + testSpec: FlickerTestParameter +) : ExitPipToAppTransition(testSpec) { + + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition(eachRun = true) { + setup { + eachRun { + // launch an app behind the pip one + testApp.launchViaIntent(wmHelper) + } + } + transitions { + // This will bring PipApp to fullscreen + pipApp.expandPipWindowToApp(wmHelper) + // Wait until the other app is no longer visible + wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName()) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 00e50e7fe3b5..1fec3cf85214 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -24,88 +23,62 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch and exit. - * To run this test: `atest WMShellFlickerTests:EnterExitPipTest` + * Test expanding a pip window back to full screen via an intent + * + * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Launch another full screen mode [testApp] + * Expand [pipApp] app to full screen via an intent + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class EnterExitPipTest( - testSpec: FlickerTestParameter -) : PipTransition(testSpec) { - private val testApp = FixedAppHelper(instrumentation) +class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) { + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true) { setup { eachRun { + // launch an app behind the pip one testApp.launchViaIntent(wmHelper) } } transitions { // This will bring PipApp to fullscreen pipApp.launchViaIntent(wmHelper) + // Wait until the other app is no longer visible + wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName()) } } - @Presubmit - @Test - fun pipAppRemainInsideVisibleBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.defaultWindowName) - } - } - - @Presubmit - @Test - fun showBothAppWindowsThenHidePip() { - testSpec.assertWm { - showsAppWindow(testApp.defaultWindowName) - .showsAppWindowOnTop(pipApp.defaultWindowName) - .then() - .hidesAppWindow(testApp.defaultWindowName) - } - } - - @Presubmit - @Test - fun showBothAppLayersThenHidePip() { - testSpec.assertLayers { - isVisible(testApp.defaultWindowName) - .isVisible(pipApp.defaultWindowName) - .then() - .isInvisible(testApp.defaultWindowName) - } - } - - @Presubmit - @Test - fun testAppCoversFullScreenWithPipOnDisplay() { - testSpec.assertLayersStart { - visibleRegion(testApp.defaultWindowName).coversExactly(displayBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds) - } - } - - @Presubmit - @Test - fun pipAppCoversFullScreen() { - testSpec.assertLayersEnd { - visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds) - } - } - companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt new file mode 100644 index 000000000000..73626c23065a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test closing a pip window via the dismiss button + * + * To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Click on the pip window + * Click on dismiss button and wait window disappear + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = { + super.transition(this, it) + transitions { + pipApp.closePipWindow(wmHelper) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt index 524a1b404591..9e43deef8d99 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt @@ -22,9 +22,9 @@ import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import org.junit.FixMethodOrder import org.junit.Test @@ -33,42 +33,58 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipCloseWithSwipe` + * Test closing a pip window by swiping it to the bottom-center of the screen + * + * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Swipe the pip window to the bottom-center of the screen and wait it disappear + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition(testSpec) { +class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { - super.transition(this, it) + get() = { args -> + super.transition(this, args) transitions { val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds val pipCenterX = pipRegion.centerX() val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 - device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 5) + device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10) + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } + wmHelper.waitForWindowSurfaceDisappeared(pipApp.component) + wmHelper.waitForAppTransitionIdle() } } @Presubmit @Test - override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible() + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() @Presubmit @Test - override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible() + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @Presubmit @Test - override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() @Presubmit @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() @FlakyTest @Test @@ -80,14 +96,29 @@ class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition @Presubmit @Test - override fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0) + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - override fun noUncoveredRegions() = super.noUncoveredRegions() + override fun entireScreenCovered() = super.entireScreenCovered() @Presubmit @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 20) + } + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt new file mode 100644 index 000000000000..d0fee9a82093 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.LAUNCHER_COMPONENT +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window by double clicking it + * + * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Expand [pipApp] app to its maximum pip size by double clicking on it + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition(eachRun = true) { + transitions { + pipApp.doubleClickPipWindow(wmHelper) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipWindowRemainInsideVisibleBounds() { + testSpec.assertWm { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipLayerRemainInsideVisibleBounds() { + testSpec.assertLayers { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks [pipApp] window remains visible throughout the animation + */ + @Presubmit + @Test + fun pipWindowIsAlwaysVisible() { + testSpec.assertWm { + isAppWindowVisible(pipApp.component) + } + } + + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit + @Test + fun pipLayerIsAlwaysVisible() { + testSpec.assertLayers { + isVisible(pipApp.component) + } + } + + /** + * Checks that the visible region of [pipApp] always expands during the animation + */ + @Presubmit + @Test + fun pipLayerExpands() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.coversAtLeast(previous.visibleRegion.region) + } + } + } + + /** + * Checks [pipApp] window remains pinned throughout the animation + */ + @Presubmit + @Test + fun windowIsAlwaysPinned() { + testSpec.assertWm { + this.invoke("hasPipWindow") { it.isPinned(pipApp.component) } + } + } + + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit + @Test + fun launcherIsAlwaysVisible() { + testSpec.assertLayers { + isVisible(LAUNCHER_COMPONENT) + } + } + + /** + * Checks that the focus doesn't change between windows during the transition + */ + @FlakyTest + @Test + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt new file mode 100644 index 000000000000..0ab857d755ee --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.traces.RegionSubject +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip movement with Launcher shelf height change (decrease). + * + * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` + * + * Actions: + * Launch [pipApp] in pip mode + * Launch [testApp] + * Press home + * Check if pip window moves down (visually) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class MovePipDownShelfHeightChangeTest( + testSpec: FlickerTestParameter +) : MovePipShelfHeightTransition(testSpec) { + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition(eachRun = false) { + teardown { + eachRun { + testApp.launchViaIntent(wmHelper) + } + test { + testApp.exit(wmHelper) + } + } + transitions { + taplInstrumentation.pressHome() + } + } + + override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { + current.isHigherOrEqual(previous.region) + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt new file mode 100644 index 000000000000..6e0324c17272 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Presubmit +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.traces.RegionSubject +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import org.junit.Test + +/** + * Base class for pip tests with Launcher shelf height change + */ +abstract class MovePipShelfHeightTransition( + testSpec: FlickerTestParameter +) : PipTransition(testSpec) { + protected val taplInstrumentation = LauncherInstrumentation() + protected val testApp = FixedAppHelper(instrumentation) + + /** + * Checks if the window movement direction is valid + */ + protected abstract fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) + + /** + * Checks [pipApp] window remains visible throughout the animation + */ + @Presubmit + @Test + open fun pipWindowIsAlwaysVisible() { + testSpec.assertWm { + isAppWindowVisible(pipApp.component) + } + } + + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit + @Test + open fun pipLayerIsAlwaysVisible() { + testSpec.assertLayers { + isVisible(pipApp.component) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipWindowRemainInsideVisibleBounds() { + testSpec.assertWm { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipLayerRemainInsideVisibleBounds() { + testSpec.assertLayers { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the visible region of [pipApp] always moves in the correct direction + * during the animation. + */ + @Presubmit + @Test + open fun pipWindowMoves() { + val windowName = pipApp.component.toWindowName() + testSpec.assertWm { + val pipWindowList = this.windowStates { it.name.contains(windowName) && it.isVisible } + pipWindowList.zipWithNext { previous, current -> + assertRegionMovement(previous.frame, current.frame) + } + } + } + + /** + * Checks that the visible region of [pipApp] always moves up during the animation + */ + @Presubmit + @Test + open fun pipLayerMoves() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + assertRegionMovement(previous.visibleRegion, current.visibleRegion) + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt index 1294ac93f647..e507edfda48c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt @@ -16,36 +16,49 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice -import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.google.common.truth.Truth +import com.android.server.wm.flicker.traces.RegionSubject import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip movement with Launcher shelf height change. - * To run this test: `atest WMShellFlickerTests:PipShelfHeightTest` + * Test Pip movement with Launcher shelf height change (increase). + * + * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` + * + * Actions: + * Launch [pipApp] in pip mode + * Press home + * Launch [testApp] + * Check if pip window moves up (visually) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - private val taplInstrumentation = LauncherInstrumentation() - private val testApp = FixedAppHelper(instrumentation) - +class MovePipUpShelfHeightChangeTest( + testSpec: FlickerTestParameter +) : MovePipShelfHeightTransition(testSpec) { + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = false) { teardown { @@ -61,33 +74,17 @@ class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpe } } - @Presubmit - @Test - fun pipAlwaysVisible() = testSpec.assertWm { this.showsAppWindow(pipApp.windowName) } - - @Presubmit - @Test - fun pipLayerInsideDisplay() { - testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds) - } - } - - @Presubmit - @Test - fun pipWindowMovesUp() = testSpec.assertWmEnd { - val initialState = this.trace?.first()?.wmState - ?: error("Trace should not be empty") - val startPos = initialState.pinnedWindows.first().frame - val currPos = this.wmState.pinnedWindows.first().frame - val subject = Truth.assertWithMessage("Pip should have moved up") - subject.that(currPos.top).isGreaterThan(startPos.top) - subject.that(currPos.bottom).isGreaterThan(startPos.bottom) - subject.that(currPos.left).isEqualTo(startPos.left) - subject.that(currPos.right).isEqualTo(startPos.right) + override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { + current.isLowerOrEqual(previous.region) } companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index d88f94d5954a..aba8aced298f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -22,12 +22,12 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.startRotation -import com.android.wm.shell.flicker.IME_WINDOW_NAME +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.helpers.ImeAppHelper import org.junit.FixMethodOrder import org.junit.Test @@ -43,7 +43,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 +@Group4 class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val imeApp = ImeAppHelper(instrumentation) @@ -79,7 +79,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) fun pipInVisibleBounds() { testSpec.assertWm { val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation) - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } @@ -90,7 +90,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) @Test fun pipIsAboveAppWindow() { testSpec.assertWmTag(TAG_IME_VISIBLE) { - isAboveWindow(IME_WINDOW_NAME, pipApp.defaultWindowName) + isAboveWindow(FlickerComponentName.IME, pipApp.component) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt index 6833b96a802b..9bea5c03dadb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt @@ -23,15 +23,20 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.helpers.ImeAppHelper -import com.android.wm.shell.flicker.helpers.FixedAppHelper import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome +import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -46,12 +51,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 161435597) -@Group3 +@Group4 class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val imeApp = ImeAppHelper(instrumentation) private val testApp = FixedAppHelper(instrumentation) + @Before + open fun setup() { + // Only run legacy split tests when the system is using legacy split screen. + assumeTrue(SplitScreenHelper.isUsingLegacySplit()) + // Legacy split is having some issue with Shell transition, and will be deprecated soon. + assumeFalse(isShellTransitionsEnabled()) + } + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { withTestName { testSpec.name } @@ -80,11 +92,11 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t } } - @Presubmit + @FlakyTest(bugId = 161435597) @Test fun pipWindowInsideDisplayBounds() { testSpec.assertWm { - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } @@ -92,25 +104,17 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(testApp.defaultWindowName) - isVisible(imeApp.defaultWindowName) - noWindowsOverlap(testApp.defaultWindowName, imeApp.defaultWindowName) + isAppWindowVisible(testApp.component) + isAppWindowVisible(imeApp.component) + doNotOverlap(testApp.component, imeApp.component) } } - @Presubmit - @Test - override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() - - @Presubmit - @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() - - @Presubmit + @FlakyTest(bugId = 161435597) @Test fun pipLayerInsideDisplayBounds() { testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } @@ -118,18 +122,14 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t @Test fun bothAppLayersVisible() { testSpec.assertLayersEnd { - visibleRegion(testApp.defaultWindowName).coversAtMost(displayBounds) - visibleRegion(imeApp.defaultWindowName).coversAtMost(displayBounds) + visibleRegion(testApp.component).coversAtMost(displayBounds) + visibleRegion(imeApp.component).coversAtMost(displayBounds) } } - @Presubmit - @Test - override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible() - - @Presubmit + @FlakyTest(bugId = 161435597) @Test - override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible() + override fun entireScreenCovered() = super.entireScreenCovered() companion object { const val TEST_REPETITIONS = 2 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index d531af28e2ad..669f37ad1e72 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -23,13 +23,13 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.noUncoveredRegions import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.wm.shell.flicker.helpers.FixedAppHelper @@ -41,17 +41,32 @@ import org.junit.runners.Parameterized /** * Test Pip Stack in bounds after rotations. + * * To run this test: `atest WMShellFlickerTests:PipRotationTest` + * + * Actions: + * Launch a [pipApp] in pip mode + * Launch another app [fixedApp] (appears below pip) + * Rotate the screen from [testSpec.config.startRotation] to [testSpec.config.endRotation] + * (usually, 0->90 and 90->0) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 +@Group4 class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val fixedApp = FixedAppHelper(instrumentation) - private val startingBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation) - private val endingBounds = WindowUtils.getDisplayBounds(testSpec.config.endRotation) + private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.config.startRotation) + private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.config.endRotation) override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = false) { configuration -> @@ -66,49 +81,104 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) transitions { setRotation(configuration.endRotation) } - teardown { - eachRun { - setRotation(Surface.ROTATION_0) - } - } } - @FlakyTest(bugId = 185400889) + /** + * Checks that all parts of the screen are covered at the start and end of the transition + */ + @Presubmit @Test - override fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation, - testSpec.config.endRotation, allStates = false) + override fun entireScreenCovered() = testSpec.entireScreenCovered() + /** + * Checks the position of the navigation bar at the start and end of the transition + */ @FlakyTest @Test - override fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() + /** + * Checks the position of the status bar at the start and end of the transition + */ @Presubmit @Test - override fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, - testSpec.config.endRotation) + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - @FlakyTest(bugId = 185400889) + /** + * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition + */ + @Presubmit @Test fun appLayerRotates_StartingBounds() { testSpec.assertLayersStart { - visibleRegion(fixedApp.defaultWindowName).coversExactly(startingBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + visibleRegion(fixedApp.component).coversExactly(screenBoundsStart) } } - @FlakyTest(bugId = 185400889) + /** + * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition + */ + @Presubmit @Test fun appLayerRotates_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(fixedApp.defaultWindowName).coversExactly(endingBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(endingBounds) + visibleRegion(fixedApp.component).coversExactly(screenBoundsEnd) + } + } + + /** + * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition + */ + @Presubmit + @Test + fun pipLayerRotates_StartingBounds() { + testSpec.assertLayersStart { + visibleRegion(pipApp.component).coversAtMost(screenBoundsStart) + } + } + + /** + * Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition + */ + @Presubmit + @Test + fun pipLayerRotates_EndingBounds() { + testSpec.assertLayersEnd { + visibleRegion(pipApp.component).coversAtMost(screenBoundsEnd) + } + } + + /** + * Ensure that the [pipApp] window does not obscure the [fixedApp] at the start of the + * transition + */ + @Presubmit + @Test + fun pipIsAboveFixedAppWindow_Start() { + testSpec.assertWmStart { + isAboveWindow(pipApp.component, fixedApp.component) + } + } + + /** + * Ensure that the [pipApp] window does not obscure the [fixedApp] at the end of the + * transition + */ + @Presubmit + @Test + fun pipIsAboveFixedAppWindow_End() { + testSpec.assertWmEnd { + isAboveWindow(pipApp.component, fixedApp.component) } } companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt deleted file mode 100644 index 55e5c4128967..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip - -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.focusChanges -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.startRotation -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipToAppTest` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition(eachRun = true) { configuration -> - setup { - eachRun { - this.setRotation(configuration.startRotation) - } - } - teardown { - eachRun { - this.setRotation(Surface.ROTATION_0) - } - } - transitions { - pipApp.expandPipWindowToApp(wmHelper) - } - } - - @FlakyTest - @Test - fun appReplacesPipWindow() { - testSpec.assertWm { - this.showsAppWindow(PIP_WINDOW_TITLE) - .then() - .showsAppWindowOnTop(pipApp.launcherName) - } - } - - @FlakyTest - @Test - fun appReplacesPipLayer() { - testSpec.assertLayers { - this.isVisible(PIP_WINDOW_TITLE) - .then() - .isVisible(pipApp.launcherName) - } - } - - @FlakyTest - @Test - fun testAppCoversFullScreen() { - testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds) - } - } - - @FlakyTest(bugId = 151179149) - @Test - fun focusChanges() = testSpec.focusChanges("NexusLauncherActivity", - pipApp.launcherName, "NexusLauncherActivity") - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt index b4c75a6d1165..e8a61e8a1dae 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt @@ -20,25 +20,24 @@ import android.app.Instrumentation import android.content.Intent import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.isRotated import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.wm.shell.flicker.helpers.PipAppHelper import com.android.wm.shell.flicker.testapp.Components import org.junit.Test @@ -162,32 +161,29 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { @Presubmit @Test - open fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + open fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + open fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - @FlakyTest + @Presubmit @Test - open fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible() + open fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() - @FlakyTest + @Presubmit @Test - open fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + open fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Presubmit @Test - open fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, Surface.ROTATION_0) + open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Presubmit @Test - open fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0) + open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - open fun noUncoveredRegions() = - testSpec.noUncoveredRegions(testSpec.config.startRotation, Surface.ROTATION_0) + open fun entireScreenCovered() = testSpec.entireScreenCovered() }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt index 1f58bb2bf9db..d6dbc366aec0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -16,14 +16,13 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE @@ -44,7 +43,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 +@Group4 class SetRequestedOrientationWhilePinnedTest( testSpec: FlickerTestParameter ) : PipTransition(testSpec) { @@ -83,55 +82,69 @@ class SetRequestedOrientationWhilePinnedTest( @FlakyTest @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + @FlakyTest + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + @FlakyTest + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + @FlakyTest + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + @FlakyTest + @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @FlakyTest @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit + @FlakyTest @Test fun pipWindowInsideDisplay() { testSpec.assertWmStart { - frameRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + frameRegion(pipApp.component).coversAtMost(startingBounds) } } - @Presubmit + @FlakyTest @Test fun pipAppShowsOnTop() { testSpec.assertWmEnd { - showsAppWindowOnTop(pipApp.defaultWindowName) + isAppWindowOnTop(pipApp.component) } } - @Presubmit + @FlakyTest @Test fun pipLayerInsideDisplay() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + visibleRegion(pipApp.component).coversAtMost(startingBounds) } } - @Presubmit + @FlakyTest @Test fun pipAlwaysVisible() = testSpec.assertWm { - this.showsAppWindow(pipApp.windowName) + this.isAppWindowVisible(pipApp.component) } - @Presubmit + @FlakyTest @Test fun pipAppLayerCoversFullScreen() { testSpec.assertLayersEnd { - visibleRegion(pipApp.defaultWindowName).coversExactly(endingBounds) + visibleRegion(pipApp.component).coversExactly(endingBounds) } } @FlakyTest @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() - } + override fun entireScreenCovered() = super.entireScreenCovered() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 0110ba3f5b30..061218a015e4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -37,14 +37,17 @@ class TvPipMenuTests : TvPipTestBase() { private val systemUiResources = packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) private val pipBoundsWhileInMenu: Rect = systemUiResources.run { - val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME)) + val bounds = getString(getIdentifier("pip_menu_bounds", "string", + SYSTEM_UI_PACKAGE_NAME)) Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") } private val playButtonDescription = systemUiResources.run { - getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME)) + getString(getIdentifier("pip_play", "string", + SYSTEM_UI_PACKAGE_NAME)) } private val pauseButtonDescription = systemUiResources.run { - getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME)) + getString(getIdentifier("pip_pause", "string", + SYSTEM_UI_PACKAGE_NAME)) } @Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt index 1b73920046dc..1c663409b913 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -70,7 +70,8 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { // descendant and then retrieve the element from the menu and return to the caller of this // method. val elementSelector = By.desc(desc) - val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector) + val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR) + .hasDescendant(elementSelector) return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS) ?.findObject(elementSelector) @@ -94,7 +95,8 @@ fun UiDevice.clickTvPipMenuFullscreenButton() { } fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) { - focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) || + focusOnAndClickTvPipMenuElement(By.desc(desc) + .pkg(SYSTEM_UI_PACKAGE_NAME)) || error("Could not focus on the Pip menu object with \"$desc\" description") // So apparently Accessibility framework on TV is not very reliable and sometimes the state of // the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml index 5549330df766..2cdbffa7589c 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -107,5 +107,20 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity + android:name=".LaunchBubbleActivity" + android:label="LaunchBubbleApp" + android:exported="true" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <action android:name="android.intent.action.VIEW" /> + </intent-filter> + </activity> + <activity + android:name=".BubbleActivity" + android:label="BubbleApp" + android:exported="false" + android:resizeableActivity="true" /> </application> </manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png Binary files differnew file mode 100644 index 000000000000..d424a17b4157 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml new file mode 100644 index 000000000000..b43f31da748d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/> + <path + android:fillColor="#FF000000" + android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/> + <path + android:fillColor="#FF000000" + android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/> +</vector> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml new file mode 100644 index 000000000000..0e8c7a0fe64a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,4c-4.97,0 -9,3.58 -9,8c0,1.53 0.49,2.97 1.33,4.18c0.12,0.18 0.2,0.46 0.1,0.66c-0.33,0.68 -0.79,1.52 -1.38,2.39c-0.12,0.17 0.01,0.41 0.21,0.39c0.63,-0.05 1.86,-0.26 3.38,-0.91c0.17,-0.07 0.36,-0.06 0.52,0.03C8.55,19.54 10.21,20 12,20c4.97,0 9,-3.58 9,-8S16.97,4 12,4zM16.94,11.63l-3.29,3.29c-0.13,0.13 -0.34,0.04 -0.34,-0.14v-1.57c0,-0.11 -0.1,-0.21 -0.21,-0.2c-2.19,0.06 -3.65,0.65 -5.14,1.95c-0.15,0.13 -0.38,0 -0.33,-0.19c0.7,-2.57 2.9,-4.57 5.5,-4.75c0.1,-0.01 0.18,-0.09 0.18,-0.19V8.2c0,-0.18 0.22,-0.27 0.34,-0.14l3.29,3.29C17.02,11.43 17.02,11.55 16.94,11.63z" + android:fillColor="#000000" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml new file mode 100644 index 000000000000..f8b0ca3da26e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <Button + android:id="@+id/button_finish" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_marginStart="8dp" + android:text="Finish" /> + <Button + android:id="@+id/button_new_task" + android:layout_width="wrap_content" + android:layout_height="46dp" + android:layout_marginStart="8dp" + android:text="New Task" /> + <Button + android:id="@+id/button_new_bubble" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:text="New Bubble" /> + + <Button + android:id="@+id/button_activity_for_result" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginStart="8dp" + android:text="Activity For Result" /> +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml new file mode 100644 index 000000000000..f23c46455c63 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/black"> + + <Button + android:id="@+id/button_create" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="Add Bubble" /> + + <Button + android:id="@+id/button_cancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/button_create" + android:layout_centerHorizontal="true" + android:layout_marginTop="20dp" + android:text="Cancel Bubble" /> + + <Button + android:id="@+id/button_cancel_all" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/button_cancel" + android:layout_centerHorizontal="true" + android:layout_marginTop="20dp" + android:text="Cancel All Bubble" /> +</RelativeLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java new file mode 100644 index 000000000000..bc3bc75ab903 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.testapp; + + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +public class BubbleActivity extends Activity { + private int mNotifId = 0; + + public BubbleActivity() { + super(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (intent != null) { + mNotifId = intent.getIntExtra(BubbleHelper.EXTRA_BUBBLE_NOTIF_ID, -1); + } else { + mNotifId = -1; + } + + setContentView(R.layout.activity_bubble); + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + String result = resultCode == Activity.RESULT_OK ? "OK" : "CANCELLED"; + Toast.makeText(this, "Activity result: " + result, Toast.LENGTH_SHORT).show(); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java new file mode 100644 index 000000000000..d743dffd3c9e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.testapp; + + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Person; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.graphics.drawable.Icon; +import android.os.SystemClock; +import android.service.notification.StatusBarNotification; +import android.view.WindowManager; + +import java.util.HashMap; + +public class BubbleHelper { + + static final String EXTRA_BUBBLE_NOTIF_ID = "EXTRA_BUBBLE_NOTIF_ID"; + static final String CHANNEL_ID = "bubbles"; + static final String CHANNEL_NAME = "Bubbles"; + static final int DEFAULT_HEIGHT_DP = 300; + + private static BubbleHelper sInstance; + + private final Context mContext; + private NotificationManager mNotificationManager; + private float mDisplayHeight; + + private HashMap<Integer, BubbleInfo> mBubbleMap = new HashMap<>(); + + private int mNextNotifyId = 0; + private int mColourIndex = 0; + + public static class BubbleInfo { + public int id; + public int height; + public Icon icon; + + public BubbleInfo(int id, int height, Icon icon) { + this.id = id; + this.height = height; + this.icon = icon; + } + } + + public static BubbleHelper getInstance(Context context) { + if (sInstance == null) { + sInstance = new BubbleHelper(context); + } + return sInstance; + } + + private BubbleHelper(Context context) { + mContext = context; + mNotificationManager = context.getSystemService(NotificationManager.class); + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription("Channel that posts bubbles"); + channel.setAllowBubbles(true); + mNotificationManager.createNotificationChannel(channel); + + Point p = new Point(); + WindowManager wm = context.getSystemService(WindowManager.class); + wm.getDefaultDisplay().getRealSize(p); + mDisplayHeight = p.y; + + } + + private int getNextNotifyId() { + int id = mNextNotifyId; + mNextNotifyId++; + return id; + } + + private Icon getIcon() { + return Icon.createWithResource(mContext, R.drawable.bg); + } + + public int addNewBubble(boolean autoExpand, boolean suppressNotif) { + int id = getNextNotifyId(); + BubbleInfo info = new BubbleInfo(id, DEFAULT_HEIGHT_DP, getIcon()); + mBubbleMap.put(info.id, info); + + Notification.BubbleMetadata data = getBubbleBuilder(info) + .setSuppressNotification(suppressNotif) + .setAutoExpandBubble(false) + .build(); + Notification notification = getNotificationBuilder(info.id) + .setBubbleMetadata(data).build(); + + mNotificationManager.notify(info.id, notification); + return info.id; + } + + private Notification.Builder getNotificationBuilder(int id) { + Person chatBot = new Person.Builder() + .setBot(true) + .setName("BubbleBot") + .setImportant(true) + .build(); + + RemoteInput remoteInput = new RemoteInput.Builder("key") + .setLabel("Reply") + .build(); + + String shortcutId = "BubbleChat"; + return new Notification.Builder(mContext, CHANNEL_ID) + .setChannelId(CHANNEL_ID) + .setShortcutId(shortcutId) + .setContentIntent(PendingIntent.getActivity(mContext, 0, + new Intent(mContext, LaunchBubbleActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT)) + .setStyle(new Notification.MessagingStyle(chatBot) + .setConversationTitle("Bubble Chat") + .addMessage("Hello? This is bubble: " + id, + SystemClock.currentThreadTimeMillis() - 300000, chatBot) + .addMessage("Is it me, " + id + ", you're looking for?", + SystemClock.currentThreadTimeMillis(), chatBot) + ) + .setSmallIcon(R.drawable.ic_bubble); + } + + private Notification.BubbleMetadata.Builder getBubbleBuilder(BubbleInfo info) { + Intent target = new Intent(mContext, BubbleActivity.class); + target.putExtra(EXTRA_BUBBLE_NOTIF_ID, info.id); + PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, info.id, target, + PendingIntent.FLAG_UPDATE_CURRENT); + + return new Notification.BubbleMetadata.Builder() + .setIntent(bubbleIntent) + .setIcon(info.icon) + .setDesiredHeight(info.height); + } + + public void cancel(int id) { + mNotificationManager.cancel(id); + } + + public void cancelAll() { + mNotificationManager.cancelAll(); + } + + public void cancelLast() { + StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications(); + if (activeNotifications.length > 0) { + mNotificationManager.cancel( + activeNotifications[activeNotifications.length - 1].getId()); + } + } + + public void cancelFirst() { + StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications(); + if (activeNotifications.length > 0) { + mNotificationManager.cancel(activeNotifications[0].getId()); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java index 0ead91bb37de..0ed59bdafd1d 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java @@ -87,4 +87,16 @@ public class Components { public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".SplitScreenSecondaryActivity"); } + + public static class LaunchBubbleActivity { + public static final String LABEL = "LaunchBubbleApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".LaunchBubbleActivity"); + } + + public static class BubbleActivity { + public static final String LABEL = "BubbleApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".BubbleActivity"); + } } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java new file mode 100644 index 000000000000..71fa66d8a61c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.testapp; + + +import android.app.Activity; +import android.app.Person; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.view.View; + +import java.util.Arrays; + +public class LaunchBubbleActivity extends Activity { + + private BubbleHelper mBubbleHelper; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addInboxShortcut(getApplicationContext()); + mBubbleHelper = BubbleHelper.getInstance(this); + setContentView(R.layout.activity_main); + findViewById(R.id.button_create).setOnClickListener(this::add); + findViewById(R.id.button_cancel).setOnClickListener(this::cancel); + findViewById(R.id.button_cancel_all).setOnClickListener(this::cancelAll); + } + + private void add(View v) { + mBubbleHelper.addNewBubble(false /* autoExpand */, false /* suppressNotif */); + } + + private void cancel(View v) { + mBubbleHelper.cancelLast(); + } + + private void cancelAll(View v) { + mBubbleHelper.cancelAll(); + } + + private void addInboxShortcut(Context context) { + Icon icon = Icon.createWithResource(this, R.drawable.bg); + Person[] persons = new Person[4]; + for (int i = 0; i < persons.length; i++) { + persons[i] = new Person.Builder() + .setBot(false) + .setIcon(icon) + .setName("google" + i) + .setImportant(true) + .build(); + } + + ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "BubbleChat") + .setShortLabel("BubbleChat") + .setLongLived(true) + .setIntent(new Intent(Intent.ACTION_VIEW)) + .setIcon(Icon.createWithResource(context, R.drawable.ic_message)) + .setPersons(persons) + .build(); + ShortcutManager scmanager = context.getSystemService(ShortcutManager.class); + scmanager.addDynamicShortcuts(Arrays.asList(shortcut)); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 6b74b620dad7..d5acbbcf7d2c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -197,6 +197,43 @@ public class ShellTaskOrganizerTests { } @Test + public void testAddListenerForMultipleTypes() { + RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo2, null); + + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, + TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_FULLSCREEN); + + // onTaskAppeared event should be delivered once for each taskInfo. + assertTrue(listener.appeared.contains(taskInfo1)); + assertTrue(listener.appeared.contains(taskInfo2)); + assertEquals(2, listener.appeared.size()); + } + + @Test + public void testRemoveListenerForMultipleTypes() { + RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo2, null); + + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, + TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_FULLSCREEN); + + mOrganizer.removeListener(listener); + + // If listener is removed properly, onTaskInfoChanged event shouldn't be delivered. + mOrganizer.onTaskInfoChanged(taskInfo1); + assertTrue(listener.infoChanged.isEmpty()); + mOrganizer.onTaskInfoChanged(taskInfo2); + assertTrue(listener.infoChanged.isEmpty()); + } + + @Test public void testWindowingModeChange() { RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java index 20ac5bf8fa84..1cbad155ba7b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java @@ -47,6 +47,8 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.wm.shell.common.HandlerExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; import org.junit.After; import org.junit.Before; @@ -71,6 +73,8 @@ public class TaskViewTest extends ShellTestCase { ShellTaskOrganizer mOrganizer; @Mock HandlerExecutor mExecutor; + @Mock + SyncTransactionQueue mSyncQueue; SurfaceSession mSession; SurfaceControl mLeash; @@ -99,7 +103,14 @@ public class TaskViewTest extends ShellTestCase { }).when(mExecutor).execute(any()); when(mOrganizer.getExecutor()).thenReturn(mExecutor); - mTaskView = new TaskView(mContext, mOrganizer); + + doAnswer((InvocationOnMock invocationOnMock) -> { + final TransactionRunnable r = invocationOnMock.getArgument(0); + r.runWithTransaction(new SurfaceControl.Transaction()); + return null; + }).when(mSyncQueue).runInSync(any()); + + mTaskView = new TaskView(mContext, mOrganizer, mSyncQueue); mTaskView.setListener(mExecutor, mViewListener); } @@ -112,7 +123,7 @@ public class TaskViewTest extends ShellTestCase { @Test public void testSetPendingListener_throwsException() { - TaskView taskView = new TaskView(mContext, mOrganizer); + TaskView taskView = new TaskView(mContext, mOrganizer, mSyncQueue); taskView.setListener(mExecutor, mViewListener); try { taskView.setListener(mExecutor, mViewListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java index 27c626170a4b..294bc1276291 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.mock; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -30,7 +31,7 @@ public class TestAppPairsController extends AppPairsController { public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, DisplayController displayController) { super(organizer, syncQueue, displayController, mock(ShellExecutor.class), - mock(DisplayImeController.class)); + mock(DisplayImeController.class), mock(DisplayInsetsController.class)); mPool = new TestAppPairsPool(this); setPairsPool(mPool); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 3e3195fe8dc5..bc701d0c70bc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -131,7 +131,7 @@ public class BubbleDataTest extends ShellTestCase { NotificationListenerService.Ranking ranking = mock(NotificationListenerService.Ranking.class); - when(ranking.visuallyInterruptive()).thenReturn(true); + when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, mMainExecutor); @@ -793,7 +793,7 @@ public class BubbleDataTest extends ShellTestCase { } @Test - public void test_expanded_removeLastBubble_collapsesStack() { + public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() { // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); changeExpandedStateAtTime(true, 2000); @@ -802,6 +802,21 @@ public class BubbleDataTest extends ShellTestCase { // Test mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); verifyUpdateReceived(); + assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0); + assertSelectionChangedTo(mBubbleData.getOverflow()); + } + + @Test + public void test_expanded_removeLastBubble_collapsesIfOverflowEmpty() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + changeExpandedStateAtTime(true, 2000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NO_BUBBLE_UP); + verifyUpdateReceived(); + assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); assertExpandedChangedTo(false); } @@ -869,6 +884,35 @@ public class BubbleDataTest extends ShellTestCase { assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA2.getKey())); } + /** + * Verifies that after the stack is collapsed with the overflow selected, it will select + * the top bubble upon next expansion. + */ + @Test + public void test_collapseWithOverflowSelected_nextExpansion() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setExpanded(true); + + mBubbleData.setListener(mListener); + + // Select the overflow + mBubbleData.setShowingOverflow(true); + mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleData.getOverflow()); + + // Collapse + mBubbleData.setExpanded(false); + verifyUpdateReceived(); + assertSelectionNotChanged(); + + // Expand (here we should select the new bubble) + mBubbleData.setExpanded(true); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA2); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); @@ -902,7 +946,7 @@ public class BubbleDataTest extends ShellTestCase { assertWithMessage("selectionChanged").that(update.selectionChanged).isFalse(); } - private void assertSelectionChangedTo(Bubble bubble) { + private void assertSelectionChangedTo(BubbleViewProvider bubble) { BubbleData.Update update = mUpdateCaptor.getValue(); assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue(); assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); @@ -925,7 +969,6 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.overflowBubbles).isEqualTo(bubbles); } - private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, NotificationListenerService.Ranking ranking) { return createBubbleEntry(userId, notifKey, packageName, ranking, 1000); @@ -971,15 +1014,15 @@ public class BubbleDataTest extends ShellTestCase { } private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime) { - sendUpdatedEntryAtTime(entry, postTime, true /* visuallyInterruptive */); + sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */); } private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, - boolean visuallyInterruptive) { + boolean textChanged) { setPostTime(entry, postTime); // BubbleController calls this: Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */); - b.setVisuallyInterruptiveForTest(visuallyInterruptive); + b.setTextChangedForTest(textChanged); // And then this mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/, true /* showInShade */); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java index 6644eaf28a62..5c1bcb9753a4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java @@ -63,7 +63,7 @@ public class BubbleFlyoutViewTest extends ShellTestCase { mFlyoutMessage.senderName = "Josh"; mFlyoutMessage.message = "Hello"; - mFlyout = new BubbleFlyoutView(getContext()); + mFlyout = new BubbleFlyoutView(getContext(), mPositioner); mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text); mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name); @@ -75,9 +75,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase { public void testShowFlyout_isVisible() { mFlyout.setupFlyoutStartingAsDot( mFlyoutMessage, - new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter, - false, - mPositioner); + new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter, + false); mFlyout.setVisibility(View.VISIBLE); assertEquals("Hello", mFlyoutText.getText()); @@ -89,9 +88,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase { public void testFlyoutHide_runsCallback() { Runnable after = mock(Runnable.class); mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage, - new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter, - false, - mPositioner); + new PointF(100, 100), true, Color.WHITE, null, after, mDotCenter, + false); mFlyout.hideFlyout(); verify(after).run(); @@ -100,9 +98,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase { @Test public void testSetCollapsePercent() { mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage, - new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter, - false, - mPositioner); + new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter, + false); mFlyout.setVisibility(View.VISIBLE); mFlyout.setCollapsePercent(1f); 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 1eba3c266358..2b9bdce45a6c 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,9 +16,12 @@ package com.android.wm.shell.bubbles.animation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + 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; @@ -36,12 +39,12 @@ 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.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Spy; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -49,26 +52,30 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC private int mDisplayWidth = 500; private int mDisplayHeight = 1000; - private int mExpandedViewPadding = 10; private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class); - @Spy ExpandedAnimationController mExpandedController; private int mStackOffset; private PointF mExpansionPoint; + private BubblePositioner mPositioner; + private BubbleStackView.StackViewState mStackViewState; @SuppressLint("VisibleForTests") @Before public void setUp() throws Exception { super.setUp(); - BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class)); - positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, + BubbleStackView stackView = mock(BubbleStackView.class); + when(stackView.getState()).thenReturn(getStackViewState()); + mPositioner = new BubblePositioner(getContext(), mock(WindowManager.class)); + mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, Insets.of(0, 0, 0, 0), new Rect(0, 0, mDisplayWidth, mDisplayHeight)); - mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding, - mOnBubbleAnimatedOutAction); + mExpandedController = new ExpandedAnimationController(mPositioner, + mOnBubbleAnimatedOutAction, + stackView); + spyOn(mExpandedController); addOneMoreThanBubbleLimitBubbles(); mLayout.setActiveController(mExpandedController); @@ -78,6 +85,13 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC mExpansionPoint = new PointF(100, 100); } + public BubbleStackView.StackViewState getStackViewState() { + mStackViewState.numberOfBubbles = mLayout.getChildCount(); + mStackViewState.selectedIndex = 0; + mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint); + return mStackViewState; + } + @Test @Ignore public void testExpansionAndCollapse() throws InterruptedException { @@ -143,11 +157,12 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC private void testBubblesInCorrectExpandedPositions() { // Check all the visible bubbles to see if they're in the right place. for (int i = 0; i < mLayout.getChildCount(); i++) { - float expectedPosition = mExpandedController.getBubbleXOrYForOrientation(i); - assertEquals(expectedPosition, + PointF expectedPosition = mPositioner.getExpandedBubbleXY(i, + getStackViewState()); + assertEquals(expectedPosition.x, mLayout.getChildAt(i).getTranslationX(), 2f); - assertEquals(expectedPosition, + assertEquals(expectedPosition.y, mLayout.getChildAt(i).getTranslationY(), 2f); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java index ef046d48e1cf..b88845044263 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -58,7 +58,7 @@ public class DisplayImeControllerTest { mT = mock(SurfaceControl.Transaction.class); mMock = mock(IInputMethodManager.class); mExecutor = spy(Runnable::run); - mPerDisplay = new DisplayImeController(null, null, mExecutor, new TransactionPool() { + mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() { @Override public SurfaceControl.Transaction acquire() { return mT; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java new file mode 100644 index 000000000000..b66c2b4aee9b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.RemoteException; +import android.util.SparseArray; +import android.view.IDisplayWindowInsetsController; +import android.view.IWindowManager; +import android.view.InsetsSourceControl; +import android.view.InsetsState; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +@SmallTest +public class DisplayInsetsControllerTest { + + private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 10; + + @Mock + private IWindowManager mWm; + @Mock + private DisplayController mDisplayController; + private DisplayInsetsController mController; + private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId; + private TestShellExecutor mExecutor; + + private ArgumentCaptor<Integer> mDisplayIdCaptor; + private ArgumentCaptor<IDisplayWindowInsetsController> mInsetsControllerCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mExecutor = new TestShellExecutor(); + mInsetsControllersByDisplayId = new SparseArray<>(); + mDisplayIdCaptor = ArgumentCaptor.forClass(Integer.class); + mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class); + mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor); + addDisplay(DEFAULT_DISPLAY); + } + + @Test + public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService() + throws RemoteException { + addDisplay(SECOND_DISPLAY); + + verify(mWm).setDisplayWindowInsetsController(eq(SECOND_DISPLAY), notNull()); + } + + @Test + public void testOnDisplayRemoved_unsetsDisplayWindowInsetsControllerInWMService() + throws RemoteException { + addDisplay(SECOND_DISPLAY); + removeDisplay(SECOND_DISPLAY); + + verify(mWm).setDisplayWindowInsetsController(SECOND_DISPLAY, null); + } + + @Test + public void testPerDisplayListenerCallback() throws RemoteException { + TrackedListener defaultListener = new TrackedListener(); + TrackedListener secondListener = new TrackedListener(); + addDisplay(SECOND_DISPLAY); + mController.addInsetsChangedListener(DEFAULT_DISPLAY, defaultListener); + mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener); + + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false); + mExecutor.flushAll(); + + assertTrue(defaultListener.topFocusedWindowChangedCount == 1); + assertTrue(defaultListener.insetsChangedCount == 1); + assertTrue(defaultListener.insetsControlChangedCount == 1); + assertTrue(defaultListener.showInsetsCount == 1); + assertTrue(defaultListener.hideInsetsCount == 1); + + assertTrue(secondListener.topFocusedWindowChangedCount == 0); + assertTrue(secondListener.insetsChangedCount == 0); + assertTrue(secondListener.insetsControlChangedCount == 0); + assertTrue(secondListener.showInsetsCount == 0); + assertTrue(secondListener.hideInsetsCount == 0); + + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false); + mExecutor.flushAll(); + + assertTrue(defaultListener.topFocusedWindowChangedCount == 1); + assertTrue(defaultListener.insetsChangedCount == 1); + assertTrue(defaultListener.insetsControlChangedCount == 1); + assertTrue(defaultListener.showInsetsCount == 1); + assertTrue(defaultListener.hideInsetsCount == 1); + + assertTrue(secondListener.topFocusedWindowChangedCount == 1); + assertTrue(secondListener.insetsChangedCount == 1); + assertTrue(secondListener.insetsControlChangedCount == 1); + assertTrue(secondListener.showInsetsCount == 1); + assertTrue(secondListener.hideInsetsCount == 1); + } + + private void addDisplay(int displayId) throws RemoteException { + mController.onDisplayAdded(displayId); + verify(mWm, times(mInsetsControllersByDisplayId.size() + 1)) + .setDisplayWindowInsetsController(mDisplayIdCaptor.capture(), + mInsetsControllerCaptor.capture()); + List<Integer> displayIds = mDisplayIdCaptor.getAllValues(); + List<IDisplayWindowInsetsController> insetsControllers = + mInsetsControllerCaptor.getAllValues(); + for (int i = 0; i < displayIds.size(); i++) { + mInsetsControllersByDisplayId.put(displayIds.get(i), insetsControllers.get(i)); + } + } + + private void removeDisplay(int displayId) { + mController.onDisplayRemoved(displayId); + mInsetsControllersByDisplayId.remove(displayId); + } + + private static class TrackedListener implements + DisplayInsetsController.OnInsetsChangedListener { + int topFocusedWindowChangedCount = 0; + int insetsChangedCount = 0; + int insetsControlChangedCount = 0; + int showInsetsCount = 0; + int hideInsetsCount = 0; + + @Override + public void topFocusedWindowChanged(String packageName) { + topFocusedWindowChangedCount++; + } + + @Override + public void insetsChanged(InsetsState insetsState) { + insetsChangedCount++; + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsControlChangedCount++; + } + + @Override + public void showInsets(int types, boolean fromIme) { + showInsetsCount++; + } + + @Override + public void hideInsets(int types, boolean fromIme) { + hideInsetsCount++; + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 952dc31cdaee..b4caeb5de4ec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -24,11 +24,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.content.res.Configuration; import android.graphics.Rect; -import android.view.SurfaceControl; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -42,6 +42,8 @@ import com.android.wm.shell.common.DisplayImeController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -50,42 +52,56 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class SplitLayoutTests extends ShellTestCase { @Mock SplitLayout.SplitLayoutHandler mSplitLayoutHandler; - @Mock SurfaceControl mRootLeash; + @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks; @Mock DisplayImeController mDisplayImeController; @Mock ShellTaskOrganizer mTaskOrganizer; + @Captor ArgumentCaptor<Runnable> mRunnableCaptor; private SplitLayout mSplitLayout; @Before public void setup() { MockitoAnnotations.initMocks(this); - mSplitLayout = new SplitLayout( + mSplitLayout = spy(new SplitLayout( "TestSplitLayout", mContext, - getConfiguration(false), + getConfiguration(), mSplitLayoutHandler, - b -> b.setParent(mRootLeash), + mCallbacks, mDisplayImeController, - mTaskOrganizer); + mTaskOrganizer)); } @Test @UiThreadTest public void testUpdateConfiguration() { - mSplitLayout.init(); - assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse(); - assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue(); + final Configuration config = getConfiguration(); + + // Verify it returns true if new config won't affect split layout. + assertThat(mSplitLayout.updateConfiguration(config)).isFalse(); + + // Verify updateConfiguration returns true if the orientation changed. + config.orientation = ORIENTATION_LANDSCAPE; + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if it rotated. + config.windowConfiguration.setRotation(1); + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if the root bounds changed. + config.windowConfiguration.setBounds(new Rect(0, 0, 2160, 1080)); + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); } @Test public void testUpdateDivideBounds() { mSplitLayout.updateDivideBounds(anyInt()); - verify(mSplitLayoutHandler).onBoundsChanging(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class)); } @Test public void testSetDividePosition() { mSplitLayout.setDividePosition(anyInt()); - verify(mSplitLayoutHandler).onBoundsChanged(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class)); } @Test @@ -96,24 +112,40 @@ public class SplitLayoutTests extends ShellTestCase { @Test @UiThreadTest - public void testSnapToDismissTarget() { + public void testSnapToDismissStart() { // verify it callbacks properly when the snap target indicates dismissing split. DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START); + mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + waitDividerFlingFinished(); verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false)); - snapTarget = getSnapTarget(0 /* position */, + } + + @Test + @UiThreadTest + public void testSnapToDismissEnd() { + // verify it callbacks properly when the snap target indicates dismissing split. + DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END); + mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + waitDividerFlingFinished(); verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true)); } - private static Configuration getConfiguration(boolean isLandscape) { + private void waitDividerFlingFinished() { + verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture()); + mRunnableCaptor.getValue().run(); + } + + private static Configuration getConfiguration() { final Configuration configuration = new Configuration(); configuration.unset(); - configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; + configuration.orientation = ORIENTATION_PORTRAIT; + configuration.windowConfiguration.setRotation(0); configuration.windowConfiguration.setBounds( - new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160)); + new Rect(0, 0, 1080, 2160)); return configuration; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java index 698315a77d8e..9bb54a18063f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.graphics.Rect; -import android.view.SurfaceControl; +import android.view.InsetsState; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -40,8 +40,8 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class SplitWindowManagerTests extends ShellTestCase { - @Mock SurfaceControl mSurfaceControl; @Mock SplitLayout mSplitLayout; + @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks; private SplitWindowManager mSplitWindowManager; @Before @@ -50,7 +50,7 @@ public class SplitWindowManagerTests extends ShellTestCase { final Configuration configuration = new Configuration(); configuration.setToDefaults(); mSplitWindowManager = new SplitWindowManager("TestSplitDivider", mContext, configuration, - b -> b.setParent(mSurfaceControl)); + mCallbacks); when(mSplitLayout.getDividerBounds()).thenReturn( new Rect(0, 0, configuration.windowConfiguration.getBounds().width(), configuration.windowConfiguration.getBounds().height())); @@ -59,7 +59,7 @@ public class SplitWindowManagerTests extends ShellTestCase { @Test @UiThreadTest public void testInitRelease() { - mSplitWindowManager.init(mSplitLayout); + mSplitWindowManager.init(mSplitLayout, new InsetsState()); assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull(); mSplitWindowManager.release(); assertThat(mSplitWindowManager.getSurfaceControl()).isNull(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index ba73d555e334..734b97b69c87 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -25,6 +25,7 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; @@ -64,6 +65,7 @@ import android.view.DisplayInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.logging.InstanceId; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -95,6 +97,9 @@ public class DragAndDropPolicyTest { @Mock private SplitScreenController mSplitScreenStarter; + @Mock + private InstanceId mLoggerSessionId; + private DisplayLayout mLandscapeDisplayLayout; private DisplayLayout mPortraitDisplayLayout; private Insets mInsets; @@ -200,7 +205,7 @@ public class DragAndDropPolicyTest { @Test public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { setRunningTask(mHomeTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); @@ -210,15 +215,15 @@ public class DragAndDropPolicyTest { } @Test - public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets() { + public void testDragAppOverFullscreenApp_expectSplitScreenTargets() { setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_TOP_OR_LEFT), any()); reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); @@ -227,15 +232,15 @@ public class DragAndDropPolicyTest { } @Test - public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets() { + public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { setRunningTask(mFullscreenAppTask); - mPolicy.start(mPortraitDisplayLayout, mActivityClipData); + mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); + mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_TOP_OR_LEFT), any()); reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); @@ -244,71 +249,61 @@ public class DragAndDropPolicyTest { } @Test - public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets() { - setRunningTask(mNonResizeableFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + public void testDragAppOverSplitApp_expectSplitTargets_DropLeft() { + setInSplitScreen(true); + setRunningTask(mSplitPrimaryAppTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_TOP_OR_LEFT), any()); } @Test - public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets() { - setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mNonResizeableActivityClipData); + public void testDragAppOverSplitApp_expectSplitTargets_DropRight() { + setInSplitScreen(true); + setRunningTask(mSplitPrimaryAppTask); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); } @Test - public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() { + public void testDragAppOverSplitAppPhone_expectVerticalSplitTargets_DropTop() { setInSplitScreen(true); setRunningTask(mSplitPrimaryAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); - reset(mSplitScreenStarter); - - // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); + eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_TOP_OR_LEFT), any()); } @Test - public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets() { + public void testDragAppOverSplitAppPhone_expectVerticalSplitTargets_DropBottom() { setInSplitScreen(true); setRunningTask(mSplitPrimaryAppTask); - mPolicy.start(mPortraitDisplayLayout, mActivityClipData); + mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); + mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); - reset(mSplitScreenStarter); - - // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); + eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); } @Test public void testTargetHitRects() { setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = mPolicy.getTargets(mInsets); for (Target t : targets) { assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java new file mode 100644 index 000000000000..d6f7e54ae369 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.fullscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.content.res.Configuration; +import android.graphics.Point; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@SmallTest +public class FullscreenTaskListenerTest { + + @Mock + private SyncTransactionQueue mSyncQueue; + @Mock + private FullscreenUnfoldController mUnfoldController; + @Mock + private SurfaceControl mSurfaceControl; + + private Optional<FullscreenUnfoldController> mFullscreenUnfoldController; + + private FullscreenTaskListener mListener; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mFullscreenUnfoldController = Optional.of(mUnfoldController); + mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController); + } + + @Test + public void testAnimatableTaskAppeared_notifiesUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0); + + mListener.onTaskAppeared(info, mSurfaceControl); + + verify(mUnfoldController).onTaskAppeared(eq(info), any()); + } + + @Test + public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() { + RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0); + RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1); + + mListener.onTaskAppeared(animatable1, mSurfaceControl); + mListener.onTaskAppeared(animatable2, mSurfaceControl); + + InOrder order = inOrder(mUnfoldController); + order.verify(mUnfoldController).onTaskAppeared(eq(animatable1), any()); + order.verify(mUnfoldController).onTaskAppeared(eq(animatable2), any()); + } + + @Test + public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); + + mListener.onTaskAppeared(info, mSurfaceControl); + + verifyNoMoreInteractions(mUnfoldController); + } + + @Test + public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); + mListener.onTaskAppeared(info, mSurfaceControl); + + mListener.onTaskInfoChanged(info); + + verifyNoMoreInteractions(mUnfoldController); + } + + @Test + public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); + mListener.onTaskAppeared(info, mSurfaceControl); + + mListener.onTaskVanished(info); + + verifyNoMoreInteractions(mUnfoldController); + } + + @Test + public void testAnimatableTaskBecameInactive_notifiesUnfoldController() { + RunningTaskInfo animatableTask = createTaskInfo(/* visible */ true, /* taskId */ 0); + mListener.onTaskAppeared(animatableTask, mSurfaceControl); + RunningTaskInfo notAnimatableTask = createTaskInfo(/* visible */ false, /* taskId */ 0); + + mListener.onTaskInfoChanged(notAnimatableTask); + + verify(mUnfoldController).onTaskVanished(eq(notAnimatableTask)); + } + + @Test + public void testAnimatableTaskVanished_notifiesUnfoldController() { + RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0); + mListener.onTaskAppeared(taskInfo, mSurfaceControl); + + mListener.onTaskVanished(taskInfo); + + verify(mUnfoldController).onTaskVanished(eq(taskInfo)); + } + + private RunningTaskInfo createTaskInfo(boolean visible, int taskId) { + final RunningTaskInfo info = spy(new RunningTaskInfo()); + info.isVisible = visible; + info.positionInParent = new Point(); + when(info.getWindowingMode()).thenReturn(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + final Configuration configuration = new Configuration(); + configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + when(info.getConfiguration()).thenReturn(configuration); + info.taskId = taskId; + return info; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java index 911fe0753845..0a3a84923053 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -42,6 +42,7 @@ import android.util.ArrayMap; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; @@ -332,6 +333,58 @@ public class OneHandedControllerTest extends OneHandedTestCase { } @Test + public void testOneHandedEnabledRotation90ShouldHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + false); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test + public void testOneHandedDisabledRotation90ShouldNotHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(false); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + false); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test + public void testSwipeToNotificationEnabledRotation90ShouldNotHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + true); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test + public void testSwipeToNotificationDisabledRotation90ShouldHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + false); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test public void testStateActive_shortcutRequestActivate_skipActions() { when(mSpiedTransitionState.getState()).thenReturn(STATE_ACTIVE); when(mSpiedTransitionState.isTransitioning()).thenReturn(false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 9d7c82bb8550..0270093da938 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -79,6 +79,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; private TestShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; + private PipTransitionState mPipTransitionState; private PipBoundsAlgorithm mPipBoundsAlgorithm; private ComponentName mComponent1; @@ -90,11 +91,12 @@ public class PipTaskOrganizerTest extends ShellTestCase { mComponent1 = new ComponentName(mContext, "component1"); mComponent2 = new ComponentName(mContext, "component2"); mPipBoundsState = new PipBoundsState(mContext); + mPipTransitionState = new PipTransitionState(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, new PipSnapAlgorithm()); mMainExecutor = new TestShellExecutor(); mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, - mMockSyncTransactionQueue, mPipBoundsState, + mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, mMockOptionalSplitScreen, mMockDisplayController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java index 1bb5fd1e49e7..2bcc45e2587d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java @@ -56,13 +56,14 @@ public class MainStageTests { MockitoAnnotations.initMocks(this); mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mSurfaceSession, null); mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); } @Test public void testActiveDeactivate() { - mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct); + mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct, + true /* reparent */); assertThat(mMainStage.isActive()).isTrue(); mMainStage.deactivate(mWct); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java index 56a005642ce2..838aa811bb87 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -29,10 +29,12 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.SyncTransactionQueue; @@ -46,7 +48,7 @@ import org.mockito.Spy; /** Tests for {@link SideStage} */ @SmallTest @RunWith(AndroidJUnit4.class) -public class SideStageTests { +public class SideStageTests extends ShellTestCase { @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; @Mock private SyncTransactionQueue mSyncQueue; @@ -57,11 +59,12 @@ public class SideStageTests { private SideStage mSideStage; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); mRootTask = new TestRunningTaskInfoBuilder().build(); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, + mSyncQueue, mSurfaceSession, null); mSideStage.onTaskAppeared(mRootTask, mRootLeash); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index ab6f76996398..f90af239db01 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -18,7 +18,6 @@ package com.android.wm.shell.splitscreen; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; - import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -33,11 +32,16 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; + +import javax.inject.Provider; + public class SplitTestUtils { static SplitLayout createMockSplitLayout() { @@ -65,9 +69,13 @@ public class SplitTestUtils { TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayImeController imeController, - SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) { + DisplayInsetsController insetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, + SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldController) { super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage, - sideStage, imeController, splitLayout, transitions, transactionPool); + sideStage, imeController, insetsController, splitLayout, transitions, + transactionPool, logger, unfoldController); // Prepare default TaskDisplayArea for testing. mDisplayAreaInfo = new DisplayAreaInfo( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index aca80f3556b9..05496b059030 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -46,10 +46,12 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -58,6 +60,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; @@ -71,6 +74,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import java.util.Optional; + /** Tests for {@link StageCoordinator} */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -79,9 +84,11 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; @Mock private DisplayImeController mDisplayImeController; + @Mock private DisplayInsetsController mDisplayInsetsController; @Mock private TransactionPool mTransactionPool; @Mock private Transitions mTransitions; @Mock private SurfaceSession mSurfaceSession; + @Mock private SplitscreenEventLogger mLogger; private SplitLayout mSplitLayout; private MainStage mMainStage; private SideStage mSideStage; @@ -92,6 +99,7 @@ public class SplitTransitionTests extends ShellTestCase { private ActivityManager.RunningTaskInfo mSideChild; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); final ShellExecutor mockExecutor = mock(ShellExecutor.class); @@ -100,14 +108,16 @@ public class SplitTransitionTests extends ShellTestCase { doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire(); mSplitLayout = SplitTestUtils.createMockSplitLayout(); mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, - mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, - mDisplayImeController, mSplitLayout, mTransitions, mTransactionPool); + mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, + mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, + mTransactionPool, + mLogger, Optional::empty); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) .when(mTransitions).startTransition(anyInt(), any(), any()); @@ -125,12 +135,13 @@ public class SplitTransitionTests extends ShellTestCase { TestRemoteTransition testRemote = new TestRemoteTransition(); IBinder transition = mSplitScreenTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), testRemote, - mStageCoordinator); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), + new RemoteTransition(testRemote), mStageCoordinator); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertTrue(accepted); @@ -168,6 +179,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskAppeared(newTask, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertFalse(accepted); assertTrue(mStageCoordinator.isSplitScreenVisible()); @@ -188,6 +200,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(newTask); accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertFalse(accepted); assertTrue(mStageCoordinator.isSplitScreenVisible()); @@ -223,6 +236,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(mSideChild); mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertFalse(mStageCoordinator.isSplitScreenVisible()); } @@ -244,6 +258,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(mSideChild); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertTrue(accepted); assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -274,6 +289,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(mSideChild); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertTrue(accepted); assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -293,13 +309,15 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo enterInfo = createEnterPairInfo(); IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new TestRemoteTransition(), mStageCoordinator); + new RemoteTransition(new TestRemoteTransition()), mStageCoordinator); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); - mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction()); + mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction(), + true /* includingTopTask */); } private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) { @@ -335,10 +353,11 @@ public class SplitTransitionTests extends ShellTestCase { @Override public void startAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) + SurfaceControl.Transaction startTransaction, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { mCalled = true; - finishCallback.onTransitionFinished(mRemoteFinishWCT); + finishCallback.onTransitionFinished(mRemoteFinishWCT, null /* sct */); } @Override diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 06b08686bf4c..cd29220bb96a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -16,17 +16,24 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.graphics.Rect; +import android.window.DisplayAreaInfo; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -37,8 +44,10 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -47,26 +56,55 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -/** Tests for {@link StageCoordinator} */ +import java.util.Optional; + +import javax.inject.Provider; + +/** + * Tests for {@link StageCoordinator} + */ @SmallTest @RunWith(AndroidJUnit4.class) public class StageCoordinatorTests extends ShellTestCase { - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; - @Mock private MainStage mMainStage; - @Mock private SideStage mSideStage; - @Mock private DisplayImeController mDisplayImeController; - @Mock private Transitions mTransitions; - @Mock private TransactionPool mTransactionPool; + @Mock + private ShellTaskOrganizer mTaskOrganizer; + @Mock + private SyncTransactionQueue mSyncQueue; + @Mock + private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + @Mock + private MainStage mMainStage; + @Mock + private SideStage mSideStage; + @Mock + private StageTaskUnfoldController mMainUnfoldController; + @Mock + private StageTaskUnfoldController mSideUnfoldController; + @Mock + private SplitLayout mSplitLayout; + @Mock + private DisplayImeController mDisplayImeController; + @Mock + private DisplayInsetsController mDisplayInsetsController; + @Mock + private Transitions mTransitions; + @Mock + private TransactionPool mTransactionPool; + @Mock + private SplitscreenEventLogger mLogger; + + private final Rect mBounds1 = new Rect(10, 20, 30, 40); + private final Rect mBounds2 = new Rect(5, 10, 15, 20); + private StageCoordinator mStageCoordinator; @Before public void setup() { MockitoAnnotations.initMocks(this); - mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, - mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, - mDisplayImeController, null /* splitLayout */, mTransitions, mTransactionPool); + mStageCoordinator = createStageCoordinator(/* splitLayout */ null); + + when(mSplitLayout.getBounds1()).thenReturn(mBounds1); + when(mSplitLayout.getBounds2()).thenReturn(mBounds2); } @Test @@ -75,12 +113,45 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator.moveToSideStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT); - verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class)); + verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class), + eq(true /* includingTopTask */)); verify(mSideStage).addTask(eq(task), any(Rect.class), any(WindowContainerTransaction.class)); } @Test + public void testDisplayAreaAppeared_initializesUnfoldControllers() { + mStageCoordinator.onDisplayAreaAppeared(mock(DisplayAreaInfo.class)); + + verify(mMainUnfoldController).init(); + verify(mSideUnfoldController).init(); + } + + @Test + public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() { + mStageCoordinator = createStageCoordinator(mSplitLayout); + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); + clearInvocations(mMainUnfoldController, mSideUnfoldController); + + mStageCoordinator.onLayoutSizeChanged(mSplitLayout); + + verify(mMainUnfoldController).onLayoutChanged(mBounds2); + verify(mSideUnfoldController).onLayoutChanged(mBounds1); + } + + @Test + public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() { + mStageCoordinator = createStageCoordinator(mSplitLayout); + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null); + clearInvocations(mMainUnfoldController, mSideUnfoldController); + + mStageCoordinator.onLayoutSizeChanged(mSplitLayout); + + verify(mMainUnfoldController).onLayoutChanged(mBounds1); + verify(mSideUnfoldController).onLayoutChanged(mBounds2); + } + + @Test public void testRemoveFromSideStage() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); @@ -90,4 +161,61 @@ public class StageCoordinatorTests extends ShellTestCase { verify(mSideStage).removeTask( eq(task.taskId), any(), any(WindowContainerTransaction.class)); } + + @Test + public void testExitSplitScreen() { + mStageCoordinator.exitSplitScreen(INVALID_TASK_ID, + SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); + verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false)); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); + } + + @Test + public void testExitSplitScreenToMainStage() { + final int testTaskId = 12345; + when(mMainStage.containsTask(eq(testTaskId))).thenReturn(true); + when(mSideStage.containsTask(eq(testTaskId))).thenReturn(false); + mStageCoordinator.exitSplitScreen(testTaskId, + SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); + verify(mMainStage).reorderChild(eq(testTaskId), eq(true), + any(WindowContainerTransaction.class)); + verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false)); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(true)); + } + + @Test + public void testExitSplitScreenToSideStage() { + final int testTaskId = 12345; + when(mMainStage.containsTask(eq(testTaskId))).thenReturn(false); + when(mSideStage.containsTask(eq(testTaskId))).thenReturn(true); + mStageCoordinator.exitSplitScreen(testTaskId, + SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); + verify(mSideStage).reorderChild(eq(testTaskId), eq(true), + any(WindowContainerTransaction.class)); + verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true)); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); + } + + private StageCoordinator createStageCoordinator(SplitLayout splitLayout) { + return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, + mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, + mDisplayImeController, mDisplayInsetsController, splitLayout, + mTransitions, mTransactionPool, mLogger, new UnfoldControllerProvider()); + } + + private class UnfoldControllerProvider implements + Provider<Optional<StageTaskUnfoldController>> { + + private boolean isMain = true; + + @Override + public Optional<StageTaskUnfoldController> get() { + if (isMain) { + isMain = false; + return Optional.of(mMainUnfoldController); + } else { + return Optional.of(mSideUnfoldController); + } + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java index 90b5b37694c6..a5746a49da2b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -21,11 +21,14 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager; +import android.os.SystemProperties; import android.view.SurfaceControl; import android.view.SurfaceSession; @@ -52,11 +55,16 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public final class StageTaskListenerTests { + private static final boolean ENABLE_SHELL_TRANSITIONS = + SystemProperties.getBoolean("persist.debug.shell_transit", false); + @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; @Mock private SyncTransactionQueue mSyncQueue; + @Mock private StageTaskUnfoldController mStageTaskUnfoldController; @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor; private SurfaceSession mSurfaceSession = new SurfaceSession(); + private SurfaceControl mSurfaceControl; private ActivityManager.RunningTaskInfo mRootTask; private StageTaskListener mStageTaskListener; @@ -68,10 +76,12 @@ public final class StageTaskListenerTests { DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mSurfaceSession, + mStageTaskUnfoldController); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootTask.parentTaskId = INVALID_TASK_ID; - mStageTaskListener.onTaskAppeared(mRootTask, new SurfaceControl()); + mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl); } @Test @@ -93,15 +103,39 @@ public final class StageTaskListenerTests { @Test public void testChildTaskAppeared() { + // With shell transitions, the transition manages status changes, so skip this test. + assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo childTask = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); - mStageTaskListener.onTaskAppeared(childTask, new SurfaceControl()); + mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl); assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue(); verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true)); } + @Test + public void testTaskAppeared_notifiesUnfoldListener() { + final ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + + mStageTaskListener.onTaskAppeared(task, mSurfaceControl); + + verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl)); + } + + @Test + public void testTaskVanished_notifiesUnfoldListener() { + final ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + mStageTaskListener.onTaskAppeared(task, mSurfaceControl); + clearInvocations(mStageTaskUnfoldController); + + mStageTaskListener.onTaskVanished(task); + + verify(mStageTaskUnfoldController).onTaskVanished(eq(task)); + } + @Test(expected = IllegalArgumentException.class) public void testUnknownTaskVanished() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); @@ -110,6 +144,8 @@ public final class StageTaskListenerTests { @Test public void testTaskVanished() { + // With shell transitions, the transition manages status changes, so skip this test. + assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo childTask = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); mStageTaskListener.mRootTaskInfo = mRootTask; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index eef0d9bb268f..e5a8aa043d1a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -30,6 +30,7 @@ import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -52,15 +53,16 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.testing.TestableContext; +import android.view.Display; import android.view.IWindowSession; import android.view.InsetsState; import android.view.Surface; -import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -101,7 +103,6 @@ public class StartingSurfaceDrawerTests { static final class TestStartingSurfaceDrawer extends StartingSurfaceDrawer{ int mAddWindowForTask = 0; - int mViewThemeResId; TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, TransactionPool pool) { @@ -109,20 +110,18 @@ public class StartingSurfaceDrawerTests { } @Override - protected boolean addWindow(int taskId, IBinder appToken, - View view, WindowManager wm, WindowManager.LayoutParams params, int suggestType) { + protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, + WindowManager.LayoutParams params, int suggestType) { // listen for addView mAddWindowForTask = taskId; - mViewThemeResId = view.getContext().getThemeResId(); // Do not wait for background color return false; } @Override - protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo) { // listen for removeView - if (mAddWindowForTask == taskId) { + if (mAddWindowForTask == removalInfo.taskId) { mAddWindowForTask = 0; } } @@ -172,9 +171,11 @@ public class StartingSurfaceDrawerTests { eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); - mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId, null, null, false); + StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo(); + removalInfo.taskId = windowInfo.taskInfo.taskId; + mStartingSurfaceDrawer.removeStartingWindow(removalInfo); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).removeWindowSynced(eq(taskId), any(), any(), eq(false)); + verify(mStartingSurfaceDrawer).removeWindowSynced(any()); assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); } @@ -183,12 +184,15 @@ public class StartingSurfaceDrawerTests { final int taskId = 1; final StartingWindowInfo windowInfo = createWindowInfo(taskId, 0); + final int[] theme = new int[1]; + doAnswer(invocation -> theme[0] = (Integer) invocation.callRealMethod()) + .when(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, STARTING_WINDOW_TYPE_SPLASH_SCREEN); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), - eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); - assertNotEquals(mStartingSurfaceDrawer.mViewThemeResId, 0); + verify(mStartingSurfaceDrawer).getSplashScreenTheme(eq(0), any()); + assertNotEquals(theme[0], 0); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java index a098a6863493..aad9528bd527 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java @@ -83,8 +83,7 @@ public class TaskSnapshotWindowTest { createTaskDescription(Color.WHITE, Color.RED, Color.BLUE), 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */, taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD, - 100 /* delayRemovalTime */, new InsetsState(), - null /* clearWindow */, new TestShellExecutor()); + new InsetsState(), null /* clearWindow */, new TestShellExecutor()); } private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 2d2ab2c9f674..e39171343bb9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -20,12 +20,20 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -48,10 +56,14 @@ import android.content.Context; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.view.IDisplayWindowListener; +import android.view.IWindowManager; +import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -65,17 +77,23 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.ArrayList; /** * Tests for the shell transitions. + * + * Build/Install/Run: + * atest WMShellUnitTests:ShellTransitionTests */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -97,8 +115,7 @@ public class ShellTransitionTests { @Test public void testBasicTransitionFlow() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); IBinder transitToken = new Binder(); @@ -117,8 +134,7 @@ public class ShellTransitionTests { @Test public void testNonDefaultHandler() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); @@ -127,11 +143,13 @@ public class ShellTransitionTests { TestTransitionHandler testHandler = new TestTransitionHandler() { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { for (TransitionInfo.Change chg : info.getChanges()) { if (chg.getMode() == TRANSIT_CHANGE) { - return super.startAnimation(transition, info, t, finishCallback); + return super.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); } } return false; @@ -199,8 +217,7 @@ public class ShellTransitionTests { @Test public void testRequestRemoteTransition() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -211,7 +228,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(remoteFinishWCT); + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } @Override @@ -222,7 +239,8 @@ public class ShellTransitionTests { }; IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, - new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, testRemote)); + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, + new RemoteTransition(testRemote))); verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); @@ -273,9 +291,76 @@ public class ShellTransitionTests { } @Test + public void testTransitionFilterNotRequirement() { + // filter that requires one opening and NO translucent apps + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = new TransitionFilter.Requirement[]{ + new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + filter.mRequirements[1].mFlags = FLAG_TRANSLUCENT; + filter.mRequirements[1].mNot = true; + + final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertTrue(filter.matches(openOnly)); + + final TransitionInfo openAndTranslucent = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + openAndTranslucent.getChanges().get(1).setFlags(FLAG_TRANSLUCENT); + assertFalse(filter.matches(openAndTranslucent)); + } + + @Test + public void testTransitionFilterChecksTypeSet() { + TransitionFilter filter = new TransitionFilter(); + filter.mTypeSet = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertTrue(filter.matches(openOnly)); + + final TransitionInfo toFrontOnly = new TransitionInfoBuilder(TRANSIT_TO_FRONT) + .addChange(TRANSIT_TO_FRONT).build(); + assertTrue(filter.matches(toFrontOnly)); + + final TransitionInfo closeOnly = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_CLOSE).build(); + assertFalse(filter.matches(closeOnly)); + } + + @Test + public void testTransitionFilterChecksFlags() { + TransitionFilter filter = new TransitionFilter(); + filter.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; + + final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK, + TRANSIT_FLAG_KEYGUARD_GOING_AWAY) + .addChange(TRANSIT_TO_BACK).build(); + assertTrue(filter.matches(withFlag)); + + final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertFalse(filter.matches(withoutFlag)); + } + + @Test + public void testTransitionFilterChecksNotFlags() { + TransitionFilter filter = new TransitionFilter(); + filter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; + + final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK, + TRANSIT_FLAG_KEYGUARD_GOING_AWAY) + .addChange(TRANSIT_TO_BACK).build(); + assertFalse(filter.matches(withFlag)); + + final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertTrue(filter.matches(withoutFlag)); + } + + @Test public void testRegisteredRemoteTransition() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -285,7 +370,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(null /* wct */); + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); } @Override @@ -300,7 +385,7 @@ public class ShellTransitionTests { new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - transitions.registerRemote(filter, testRemote); + transitions.registerRemote(filter, new RemoteTransition(testRemote)); mMainExecutor.flushAll(); IBinder transitToken = new Binder(); @@ -320,8 +405,7 @@ public class ShellTransitionTests { @Test public void testOneShotRemoteHandler() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -332,7 +416,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(remoteFinishWCT); + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } @Override @@ -344,11 +428,12 @@ public class ShellTransitionTests { final int transitType = TRANSIT_FIRST_CUSTOM + 1; - OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, testRemote); + OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, + new RemoteTransition(testRemote)); // Verify that it responds to the remote but not other things. IBinder transitToken = new Binder(); assertNotNull(oneShot.handleRequest(transitToken, - new TransitionRequestInfo(transitType, null, testRemote))); + new TransitionRequestInfo(transitType, null, new RemoteTransition(testRemote)))); assertNull(oneShot.handleRequest(transitToken, new TransitionRequestInfo(transitType, null, null))); @@ -358,15 +443,16 @@ public class ShellTransitionTests { oneShot.setTransition(transitToken); IBinder anotherToken = new Binder(); assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0), - mock(SurfaceControl.Transaction.class), testFinish)); + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + testFinish)); assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0), - mock(SurfaceControl.Transaction.class), testFinish)); + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + testFinish)); } @Test public void testTransitionQueueing() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); IBinder transitToken1 = new Binder(); @@ -406,8 +492,7 @@ public class ShellTransitionTests { @Test public void testTransitionMerging() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); mDefaultHandler.setSimulateMerge(true); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -443,11 +528,80 @@ public class ShellTransitionTests { assertEquals(0, mDefaultHandler.activeCount()); } + @Test + public void testShouldRotateSeamlessly() throws Exception { + final RunningTaskInfo taskInfo = + createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + final RunningTaskInfo taskInfoPip = + createTaskInfo(1, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); + + final DisplayController displays = createTestDisplayController(); + final @Surface.Rotation int upsideDown = displays + .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation(); + + final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() + .build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays)); + + // Seamless if all tasks are seamless + final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() + .build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays)); + + // Not seamless if there is PiP (or any other non-seamless task) + final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() + .build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip) + .setRotate().build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays)); + + // Not seamless if one of rotations is upside-down + final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays)); + + // Not seamless if system alert windows + final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags( + FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays)); + + // Not seamless if there is no changed task. + final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate().build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays)); + } + class TransitionInfoBuilder { final TransitionInfo mInfo; TransitionInfoBuilder(@WindowManager.TransitionType int type) { - mInfo = new TransitionInfo(type, 0 /* flags */); + this(type, 0 /* flags */); + } + + TransitionInfoBuilder(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags) { + mInfo = new TransitionInfo(type, flags); mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0); } @@ -465,11 +619,53 @@ public class ShellTransitionTests { return addChange(mode, null /* taskInfo */); } + TransitionInfoBuilder addChange(TransitionInfo.Change change) { + mInfo.addChange(change); + return this; + } + TransitionInfo build() { return mInfo; } } + class ChangeBuilder { + final TransitionInfo.Change mChange; + + ChangeBuilder(@WindowManager.TransitionType int mode) { + mChange = new TransitionInfo.Change(null /* token */, null /* leash */); + mChange.setMode(mode); + } + + ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) { + mChange.setFlags(flags); + return this; + } + + ChangeBuilder setTask(RunningTaskInfo taskInfo) { + mChange.setTaskInfo(taskInfo); + return this; + } + + ChangeBuilder setRotate(int anim) { + return setRotate(Surface.ROTATION_90, anim); + } + + ChangeBuilder setRotate() { + return setRotate(ROTATION_ANIMATION_UNSPECIFIED); + } + + ChangeBuilder setRotate(@Surface.Rotation int target, int anim) { + mChange.setRotation(Surface.ROTATION_0, target); + mChange.setRotationAnimation(anim); + return this; + } + + TransitionInfo.Change build() { + return mChange; + } + } + class TestTransitionHandler implements Transitions.TransitionHandler { ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>(); final ArrayList<IBinder> mMerged = new ArrayList<>(); @@ -477,7 +673,8 @@ public class ShellTransitionTests { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { mFinishes.add(finishCallback); return true; @@ -540,4 +737,34 @@ public class ShellTransitionTests { return taskInfo; } + private DisplayController createTestDisplayController() { + IWindowManager mockWM = mock(IWindowManager.class); + final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1]; + try { + doReturn(new int[] {DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any()); + } catch (RemoteException e) { + // No remote stuff happening, so this can't be hit + } + DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor); + out.initialize(); + return out; + } + + private Transitions createTestTransitions() { + return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(), + mContext, mMainExecutor, mAnimExecutor); + } +// +// private class TestDisplayController extends DisplayController { +// private final DisplayLayout mTestDisplayLayout; +// TestDisplayController() { +// super(mContext, mock(IWindowManager.class), mMainExecutor); +// mTestDisplayLayout = new DisplayLayout(); +// mTestDisplayLayout. +// } +// +// @Override +// DisplayLayout +// } + } diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 2c299fa32315..2b31bcf78890 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -570,6 +570,7 @@ cc_defaults { "renderthread/DrawFrameTask.cpp", "renderthread/EglManager.cpp", "renderthread/ReliableSurface.cpp", + "renderthread/RenderEffectCapabilityQuery.cpp", "renderthread/VulkanManager.cpp", "renderthread/VulkanSurface.cpp", "renderthread/RenderProxy.cpp", @@ -696,6 +697,7 @@ cc_test { "tests/unit/MatrixTests.cpp", "tests/unit/OpBufferTests.cpp", "tests/unit/PathInterpolatorTests.cpp", + "tests/unit/RenderEffectCapabilityQueryTests.cpp", "tests/unit/RenderNodeDrawableTests.cpp", "tests/unit/RenderNodeTests.cpp", "tests/unit/RenderPropertiesTests.cpp", diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 35449875d324..475fd700ccc9 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -50,7 +50,8 @@ bool Properties::showDirtyRegions = false; bool Properties::skipEmptyFrames = true; bool Properties::useBufferAge = true; bool Properties::enablePartialUpdates = true; -bool Properties::enableRenderEffectCache = false; +// Default true unless otherwise specified in RenderThread Configuration +bool Properties::enableRenderEffectCache = true; DebugLevel Properties::debugLevel = kDebugDisabled; OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default; diff --git a/libs/hwui/apex/android_matrix.cpp b/libs/hwui/apex/android_matrix.cpp index 693b22b62663..04ac3cf0ebc8 100644 --- a/libs/hwui/apex/android_matrix.cpp +++ b/libs/hwui/apex/android_matrix.cpp @@ -35,3 +35,10 @@ bool AMatrix_getContents(JNIEnv* env, jobject matrixObj, float values[9]) { } return false; } + +jobject AMatrix_newInstance(JNIEnv* env, float values[9]) { + jobject matrixObj = android::android_graphics_Matrix_newInstance(env); + SkMatrix* m = android::android_graphics_Matrix_getSkMatrix(env, matrixObj); + m->set9(values); + return matrixObj; +} diff --git a/libs/hwui/apex/include/android/graphics/matrix.h b/libs/hwui/apex/include/android/graphics/matrix.h index 987ad13f7635..5705ba485ba3 100644 --- a/libs/hwui/apex/include/android/graphics/matrix.h +++ b/libs/hwui/apex/include/android/graphics/matrix.h @@ -34,6 +34,16 @@ __BEGIN_DECLS */ ANDROID_API bool AMatrix_getContents(JNIEnv* env, jobject matrixObj, float values[9]); +/** + * Returns a new Matrix jobject that contains the values passed in as initial values. + * @param values The 9 values of the 3x3 matrix in the following order. + * values[0] = scaleX values[1] = skewX values[2] = transX + * values[3] = skewY values[4] = scaleY values[5] = transY + * values[6] = persp0 values[7] = persp1 values[8] = persp2 + * @return The matrix jobject + */ +ANDROID_API jobject AMatrix_newInstance(JNIEnv* env, float values[9]); + __END_DECLS #endif // ANDROID_GRAPHICS_MATRIX_H diff --git a/libs/hwui/jni/android_graphics_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp index 7338ef24cb58..cf6702e45fff 100644 --- a/libs/hwui/jni/android_graphics_Matrix.cpp +++ b/libs/hwui/jni/android_graphics_Matrix.cpp @@ -378,13 +378,17 @@ static const JNINativeMethod methods[] = { {"nEquals", "(JJ)Z", (void*) SkMatrixGlue::equals} }; +static jclass sClazz; static jfieldID sNativeInstanceField; +static jmethodID sCtor; int register_android_graphics_Matrix(JNIEnv* env) { int result = RegisterMethodsOrDie(env, "android/graphics/Matrix", methods, NELEM(methods)); jclass clazz = FindClassOrDie(env, "android/graphics/Matrix"); + sClazz = MakeGlobalRefOrDie(env, clazz); sNativeInstanceField = GetFieldIDOrDie(env, clazz, "native_instance", "J"); + sCtor = GetMethodIDOrDie(env, clazz, "<init>", "()V"); return result; } @@ -393,4 +397,7 @@ SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj) { return reinterpret_cast<SkMatrix*>(env->GetLongField(matrixObj, sNativeInstanceField)); } +jobject android_graphics_Matrix_newInstance(JNIEnv* env) { + return env->NewObject(sClazz, sCtor); +} } diff --git a/libs/hwui/jni/android_graphics_Matrix.h b/libs/hwui/jni/android_graphics_Matrix.h index fe90d2ef945d..79de48b46954 100644 --- a/libs/hwui/jni/android_graphics_Matrix.h +++ b/libs/hwui/jni/android_graphics_Matrix.h @@ -25,6 +25,9 @@ namespace android { /* Gets the underlying SkMatrix from a Matrix object. */ SkMatrix* android_graphics_Matrix_getSkMatrix(JNIEnv* env, jobject matrixObj); +/* Creates a new Matrix java object. */ +jobject android_graphics_Matrix_newInstance(JNIEnv* env); + } // namespace android #endif // _ANDROID_GRAPHICS_MATRIX_H_ diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt index 73de0d12a60b..77b8a44d85a1 100644 --- a/libs/hwui/libhwui.map.txt +++ b/libs/hwui/libhwui.map.txt @@ -28,6 +28,7 @@ LIBHWUI { register_android_graphics_GraphicsStatsService; zygote_preload_graphics; AMatrix_getContents; + AMatrix_newInstance; APaint_createPaint; APaint_destroyPaint; APaint_setBlendMode; diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index 383c79b27918..c7d7a17a23eb 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -28,6 +28,7 @@ #include "Frame.h" #include "Properties.h" +#include "RenderEffectCapabilityQuery.h" #include "utils/Color.h" #include "utils/StringUtils.h" @@ -148,7 +149,11 @@ void EglManager::initialize() { mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension; auto* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR)); - Properties::enableRenderEffectCache = (strcmp(vendor, "Qualcomm") != 0); + auto* version = reinterpret_cast<const char*>(glGetString(GL_VERSION)); + Properties::enableRenderEffectCache = supportsRenderEffectCache( + vendor, version); + ALOGV("RenderEffectCache supported %d on driver version %s", + Properties::enableRenderEffectCache, version); } EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavior swapBehavior) { diff --git a/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp b/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp new file mode 100644 index 000000000000..a003988575c8 --- /dev/null +++ b/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 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. + */ +#include <stdio.h> +#include <string.h> +#include <utils/Log.h> + +bool supportsRenderEffectCache(const char* vendor, const char* version) { + if (strcmp(vendor, "Qualcomm") != 0) { + return true; + } + + int major; + int minor; + int driverMajor; + int driverMinor; + int n = sscanf(version,"OpenGL ES %d.%d V@%d.%d", + &major, + &minor, + &driverMajor, + &driverMinor); + // Ensure we have parsed the vendor string properly and we have either + // a newer major driver version, or the minor version is rev'ed + // Based on b/198227600#comment5 it appears that the corresponding fix + // is in driver version 571.0 + return n == 4 && driverMajor >= 571; +}
\ No newline at end of file diff --git a/libs/hwui/renderthread/RenderEffectCapabilityQuery.h b/libs/hwui/renderthread/RenderEffectCapabilityQuery.h new file mode 100644 index 000000000000..ea673dd0386d --- /dev/null +++ b/libs/hwui/renderthread/RenderEffectCapabilityQuery.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 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. + */ + +#pragma once + +/** + * Verify if the provided vendor and version supports RenderEffect caching + * behavior. + * + * Certain Open GL Driver implementations run into blocking scenarios + * with Fence::waitForever without a corresponding signal to unblock + * This happens during attempts to cache SkImage instances across frames + * especially in circumstances using RenderEffect/SkImageFilter internally. + * So detect the corresponding GL Vendor and driver version to determine if + * caching SkImage instances across frames is supported. + * See b/197263715 & b/193145089 + * @param vendor Vendor of the GL driver + * @param version Version of the GL driver from the given vendor + * @return True if a RenderEffect result can be cached across frames, + * false otherwise + */ +bool supportsRenderEffectCache(const char* vendor, const char* version); diff --git a/libs/hwui/tests/unit/EglManagerTests.cpp b/libs/hwui/tests/unit/EglManagerTests.cpp index f7f240663397..7f2e1589ae6c 100644 --- a/libs/hwui/tests/unit/EglManagerTests.cpp +++ b/libs/hwui/tests/unit/EglManagerTests.cpp @@ -17,6 +17,7 @@ #include <gtest/gtest.h> #include "renderthread/EglManager.h" +#include "renderthread/RenderEffectCapabilityQuery.h" #include "tests/common/TestContext.h" using namespace android; @@ -41,4 +42,17 @@ TEST(EglManager, doesSurfaceLeak) { } eglManager.destroy(); +} + +TEST(EglManager, verifyRenderEffectCacheSupported) { + EglManager eglManager; + eglManager.initialize(); + auto* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR)); + auto* version = reinterpret_cast<const char*>(glGetString(GL_VERSION)); + // Make sure that EglManager initializes Properties::enableRenderEffectCache + // based on the given gl vendor and version within EglManager->initialize() + bool renderEffectCacheSupported = supportsRenderEffectCache(vendor, version); + EXPECT_EQ(renderEffectCacheSupported, + Properties::enableRenderEffectCache); + eglManager.destroy(); }
\ No newline at end of file diff --git a/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp b/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp new file mode 100644 index 000000000000..0ee654929b3b --- /dev/null +++ b/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 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. + */ + +#include <gtest/gtest.h> +#include "renderthread/RenderEffectCapabilityQuery.h" +#include "tests/common/TestContext.h" + +TEST(RenderEffectCapabilityQuery, testSupportedVendor) { + ASSERT_TRUE(supportsRenderEffectCache("Google", "OpenGL ES 1.4 V@0.0")); +} + +TEST(RenderEffectCapabilityQuery, testSupportedVendorWithDifferentVersion) { + ASSERT_TRUE(supportsRenderEffectCache("Google", "OpenGL ES 1.3 V@571.0")); +} + +TEST(RenderEffectCapabilityQuery, testVendorWithSupportedVersion) { + ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.0")); +} + +TEST(RenderEffectCapabilityQuery, testVendorWithSupportedPatchVersion) { + ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.1")); +} + +TEST(RenderEffectCapabilityQuery, testVendorWithNewerThanSupportedMajorVersion) { + ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@572.0")); +} + +TEST(RenderEffectCapabilityQuery, testVendorWithNewerThanSupportedMinorVersion) { + ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.2")); +} + +TEST(RenderEffectCapabilityQuery, testVendorWithUnsupportedMajorVersion) { + ASSERT_FALSE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.0 V@570.1")); +} + +TEST(RenderEffectCapabilityQuery, testVendorWithUnsupportedVersion) { + ASSERT_FALSE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.1 V@570.0")); +} + diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index acd8bced0612..d10e68816d28 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -153,8 +153,7 @@ void SpriteController::doUpdateSprites() { || update.state.surfaceHeight < desiredHeight) { needApplyTransaction = true; - t.setSize(update.state.surfaceControl, - desiredWidth, desiredHeight); + update.state.surfaceControl->updateDefaultBufferSize(desiredWidth, desiredHeight); update.state.surfaceWidth = desiredWidth; update.state.surfaceHeight = desiredHeight; update.state.surfaceDrawn = false; |