diff options
Diffstat (limited to 'libs')
179 files changed, 9064 insertions, 1911 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java index b7a60392c512..ce4e10364ba2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/ExtensionProvider.java @@ -18,6 +18,10 @@ package androidx.window.extensions; import android.content.Context; +import androidx.annotation.NonNull; +import androidx.window.extensions.embedding.ActivityEmbeddingComponent; +import androidx.window.extensions.organizer.EmbeddingExtensionImpl; + /** * 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. @@ -31,6 +35,12 @@ public class ExtensionProvider { return new SampleExtensionImpl(context); } + /** Provides a reference implementation of {@link ActivityEmbeddingComponent}. */ + public static ActivityEmbeddingComponent getActivityEmbeddingExtensionImpl( + @NonNull Context context) { + return new EmbeddingExtensionImpl(); + } + /** * The support library will use this method to check API version compatibility. * @return API version string in MAJOR.MINOR.PATCH-description format. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java new file mode 100644 index 000000000000..9a8961f1d460 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/EmbeddingExtensionImpl.java @@ -0,0 +1,48 @@ +/* + * 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.organizer; + +import androidx.annotation.NonNull; +import androidx.window.extensions.embedding.ActivityEmbeddingComponent; +import androidx.window.extensions.embedding.EmbeddingRule; +import androidx.window.extensions.embedding.SplitInfo; + +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Reference implementation of the activity embedding interface defined in WM Jetpack. + */ +public class EmbeddingExtensionImpl implements ActivityEmbeddingComponent { + + private final SplitController mSplitController; + + public EmbeddingExtensionImpl() { + mSplitController = new SplitController(); + } + + @Override + public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { + mSplitController.setEmbeddingRules(rules); + } + + @Override + public void setEmbeddingCallback(@NonNull Consumer<List<SplitInfo>> consumer) { + mSplitController.setEmbeddingCallback(consumer); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java new file mode 100644 index 000000000000..9212a0f5e6b9 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java @@ -0,0 +1,305 @@ +/* + * 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.organizer; + +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 androidx.window.extensions.embedding.SplitRule; + +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.TaskFragmentAdjacentOptions adjacentOptions = null; + final boolean finishSecondaryWithPrimary = + splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule); + final boolean finishPrimaryWithSecondary = + splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule); + if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) { + adjacentOptions = new WindowContainerTransaction.TaskFragmentAdjacentOptions(); + adjacentOptions.setDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary); + adjacentOptions.setDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary); + } + wct.setAdjacentTaskFragments(primary, secondary, adjacentOptions); + } + + 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/organizer/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java new file mode 100644 index 000000000000..4fd2126dfa27 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.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 androidx.window.extensions.organizer; + +import android.annotation.NonNull; +import android.app.Activity; + +import androidx.window.extensions.embedding.SplitPairRule; +import androidx.window.extensions.embedding.SplitPlaceholderRule; +import androidx.window.extensions.embedding.SplitRule; + +/** + * 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)) { + 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/organizer/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java new file mode 100644 index 000000000000..05c6792a3fc7 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java @@ -0,0 +1,774 @@ +/* + * 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.organizer; + +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.util.Pair; +import android.window.TaskFragmentAppearedInfo; +import android.window.TaskFragmentInfo; +import android.window.WindowContainerTransaction; + +import androidx.window.extensions.embedding.ActivityRule; +import androidx.window.extensions.embedding.EmbeddingRule; +import androidx.window.extensions.embedding.SplitInfo; +import androidx.window.extensions.embedding.SplitPairRule; +import androidx.window.extensions.embedding.SplitPlaceholderRule; +import androidx.window.extensions.embedding.SplitRule; +import androidx.window.extensions.embedding.TaskFragment; + +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 { + + 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; + + 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. */ + 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. + */ + public void setEmbeddingCallback(@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()); + } + + @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()) { + mPresenter.cleanupContainer(container, true /* 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(); + } + } + + /** + * 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 onActivityCreated(@NonNull Activity launchedActivity) { + final List<EmbeddingRule> splitRules = getSplitRules(); + final TaskFragmentContainer currentContainer = getContainerWithActivity( + launchedActivity.getActivityToken(), launchedActivity); + + // Check if the activity is configured to always be expanded. + if (shouldExpand(launchedActivity, 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); + + updateCallbackIfNecessary(); + } + + 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) { + return getContainerWithActivity(activityToken, null /* activityToAdd */); + } + + /** + * This method can only be called from {@link #onActivityCreated(Activity)}, use + * {@link #getContainerWithActivity(IBinder) } otherwise. + * + * Returns a container that this activity is registered with. The activity could be created + * before the container appeared, adding the activity to the container if so. + */ + @Nullable + private TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken, + Activity activityToAdd) { + final IBinder taskFragmentToken = ActivityThread.currentActivityThread().getActivityClient( + activityToken).mInitialTaskFragmentToken; + for (TaskFragmentContainer container : mContainers) { + if (container.hasActivity(activityToken)) { + return container; + } else if (container.getTaskFragmentToken().equals(taskFragmentToken)) { + if (activityToAdd != null) { + container.addPendingAppearedActivity(activityToAdd); + } + 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.getActivityPredicate().test(activity)) { + return placeholderRule; + } + } + return null; + } + + /** + * Notifies listeners about changes to split states if necessary. + */ + private void updateCallbackIfNecessary() { + if (mEmbeddingCallback == null) { + return; + } + // TODO(b/190433398): Check if something actually changed + mEmbeddingCallback.accept(getActiveSplitStates()); + } + + /** + * Returns a list of descriptors for currently active split states. + */ + private List<SplitInfo> getActiveSplitStates() { + List<SplitInfo> splitStates = new ArrayList<>(); + for (SplitContainer container : mSplitContainers) { + TaskFragment primaryContainer = + new TaskFragment( + container.getPrimaryContainer().collectActivities()); + TaskFragment secondaryContainer = + new TaskFragment( + container.getSecondaryContainer().collectActivities()); + SplitInfo splitState = new SplitInfo(primaryContainer, + secondaryContainer, container.getSplitRule().getSplitRatio()); + splitStates.add(splitState); + } + return splitStates; + } + + /** + * 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.getActivityIntentPredicate().test( + new Pair(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.getActivityPairPredicate().test( + new Pair(primaryActivity, secondaryActivity)) + && (intent == null || pairRule.getActivityIntentPredicate().test( + new Pair(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(@NonNull Activity activity, + 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 (activityRule.getActivityPredicate().test(activity)) { + 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 handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable r) { + handler.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 (!setLaunchingToSideContainer(launchingActivity, intent, options)) { + setLaunchingInSameContainer(launchingActivity, intent, options); + } + + return super.onStartActivity(who, intent, options); + } + + /** + * 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/organizer/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java new file mode 100644 index 000000000000..ac85ac8cbc34 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java @@ -0,0 +1,353 @@ +/* + * 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.organizer; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +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 androidx.window.extensions.embedding.SplitPairRule; +import androidx.window.extensions.embedding.SplitRule; + +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_LEFT = 0; + private static final int POSITION_RIGHT = 1; + private static final int POSITION_FILL = 2; + + @IntDef(value = { + POSITION_LEFT, + POSITION_RIGHT, + 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_LEFT, parentBounds, rule); + final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, + primaryActivity, primaryRectBounds, null); + + // Create new empty task fragment + TaskFragmentContainer secondaryContainer = mController.newContainer(null); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, 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_LEFT, parentBounds, rule); + final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, + primaryActivity, primaryRectBounds, null); + + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, 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 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_LEFT, parentBounds, rule); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, 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 Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule); + final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule); + + // If the task fragments are not registered yet, the positions will be updated after they + // are created again. + resizeTaskFragmentIfRegistered(wct, splitContainer.getPrimaryContainer(), + primaryRectBounds); + resizeTaskFragmentIfRegistered(wct, splitContainer.getSecondaryContainer(), + secondaryRectBounds); + } + + /** + * 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.getParentWindowMetricsPredicate().test(parentMetrics); + } + + @NonNull + private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds, + @NonNull SplitRule rule) { + if (!shouldShowSideBySide(parentBounds, rule)) { + return new Rect(); + } + + float splitRatio = rule.getSplitRatio(); + switch (position) { + case POSITION_LEFT: + return new Rect( + parentBounds.left, + parentBounds.top, + (int) (parentBounds.left + parentBounds.width() * splitRatio), + parentBounds.bottom); + case POSITION_RIGHT: + return new Rect( + (int) (parentBounds.left + parentBounds.width() * splitRatio), + parentBounds.top, + parentBounds.right, + parentBounds.bottom); + case POSITION_FILL: + return parentBounds; + } + return parentBounds; + } + + @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/organizer/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java new file mode 100644 index 000000000000..b85287d8a919 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationController.java @@ -0,0 +1,61 @@ +/* + * 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.organizer; + +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"; + // TODO(b/196173550) turn off when finalize + 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); + 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/organizer/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java new file mode 100644 index 000000000000..9ee60d8c6bd3 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentAnimationRunner.java @@ -0,0 +1,93 @@ +/* + * 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.organizer; + +import static android.view.RemoteAnimationTarget.MODE_OPENING; + +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 androidx.annotation.Nullable; + +/** To run the TaskFragment animations. */ +class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { + + private static final String TAG = "TaskFragAnimationRunner"; + private final Handler mHandler = new Handler(Looper.myLooper()); + + @Nullable + private IRemoteAnimationFinishedCallback mFinishedCallback; + + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + 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(apps, finishedCallback)); + } + + @Override + public void onAnimationCancelled() { + if (TaskFragmentAnimationController.DEBUG) { + Log.v(TAG, "onAnimationCancelled"); + } + mHandler.post(this::onAnimationFinished); + } + + private void startAnimation(RemoteAnimationTarget[] targets, + IRemoteAnimationFinishedCallback finishedCallback) { + // TODO(b/196173550) replace with actual animations + mFinishedCallback = finishedCallback; + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (RemoteAnimationTarget target : targets) { + if (target.mode == MODE_OPENING) { + t.show(target.leash); + t.setAlpha(target.leash, 1); + } + t.setPosition(target.leash, target.localBounds.left, target.localBounds.top); + } + t.apply(); + onAnimationFinished(); + } + + private void onAnimationFinished() { + if (mFinishedCallback == null) { + return; + } + try { + mFinishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + mFinishedCallback = null; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java new file mode 100644 index 000000000000..a4f5c75276f5 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java @@ -0,0 +1,246 @@ +/* + * 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.organizer; + +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; + } + + @Nullable + TaskFragmentInfo getInfo() { + return mInfo; + } + + void setInfo(@Nullable 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) { + return; + } + mIsFinished = true; + + // Finish own activities + for (Activity activity : collectActivities()) { + activity.finish(); + } + + // Cleanup the visuals + presenter.deleteTaskFragment(wct, getTaskFragmentToken()); + // Cleanup the records + controller.removeContainer(this); + + 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/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex be6652d43fb2..097febf9770a 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar 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..84f4fdff4e1a --- /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_neutral2_500" android:lStar="35" /> +</selector>
\ No newline at end of file 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/drawable/split_rounded_bottom.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_bottom.xml new file mode 100644 index 000000000000..18dc909ae955 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/split_rounded_bottom.xml @@ -0,0 +1,30 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/split_divider_corner_size" + android:height="@dimen/split_divider_corner_size" + android:viewportWidth="42" + android:viewportHeight="42"> + + <group android:pivotX="21" + android:pivotY="21" + android:rotation="180"> + <path + android:fillColor="@color/split_divider_background" + android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" /> + </group> + +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_left.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_left.xml new file mode 100644 index 000000000000..931cacf887cd --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/split_rounded_left.xml @@ -0,0 +1,30 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/split_divider_corner_size" + android:height="@dimen/split_divider_corner_size" + android:viewportWidth="42" + android:viewportHeight="42"> + + <group android:pivotX="21" + android:pivotY="21" + android:rotation="-90"> + <path + android:fillColor="@color/split_divider_background" + android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" /> + </group> + +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_right.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_right.xml new file mode 100644 index 000000000000..54e47612faa8 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/split_rounded_right.xml @@ -0,0 +1,30 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/split_divider_corner_size" + android:height="@dimen/split_divider_corner_size" + android:viewportWidth="42" + android:viewportHeight="42"> + + <group android:pivotX="21" + android:pivotY="21" + android:rotation="90"> + <path + android:fillColor="@color/split_divider_background" + android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" /> + </group> + +</vector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/split_rounded_top.xml b/libs/WindowManager/Shell/res/drawable/split_rounded_top.xml new file mode 100644 index 000000000000..9115b5a2352e --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/split_rounded_top.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/split_divider_corner_size" + android:height="@dimen/split_divider_corner_size" + android:viewportWidth="42" + android:viewportHeight="42"> + + <path + android:fillColor="@color/split_divider_background" + android:pathData="m 0 0 c 8 0 16 8 16 16 h 10 c 0 -8 8 -16 16 -16 z" /> + +</vector>
\ No newline at end of file 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..94182cdba0dd 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"/> - - <com.android.wm.shell.common.split.DividerHandleView - style="@style/DockedDividerHandle" - android:id="@+id/docked_divider_handle" - android:contentDescription="@string/accessibility_divider" - android:background="@null"/> + <FrameLayout + android:id="@+id/divider_bar" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <View style="@style/DockedDividerTopLeftRoundCorner"/> + + <View + style="@style/DockedDividerBackground" + android:id="@+id/docked_divider_background"/> + + <View style="@style/DockedDividerBottomRightRoundCorner"/> + + <com.android.wm.shell.common.split.DividerHandleView + style="@style/DockedDividerHandle" + android:id="@+id/docked_divider_handle" + android:contentDescription="@string/accessibility_divider" + android:background="@null"/> + + </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..4e2a77f213a0 --- /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. +--> +<com.android.wm.shell.splitscreen.OutlineRoot + 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" /> + +</com.android.wm.shell.splitscreen.OutlineRoot> 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..e5707f3170d8 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"> @@ -27,6 +28,20 @@ <item name="android:layout_height">96dp</item> </style> + <style name="DockedDividerTopLeftRoundCorner"> + <item name="android:layout_gravity">center_horizontal|top</item> + <item name="android:background">@drawable/split_rounded_top</item> + <item name="android:layout_width">@dimen/split_divider_corner_size</item> + <item name="android:layout_height">@dimen/split_divider_corner_size</item> + </style> + + <style name="DockedDividerBottomRightRoundCorner"> + <item name="android:layout_gravity">center_horizontal|bottom</item> + <item name="android:background">@drawable/split_rounded_bottom</item> + <item name="android:layout_width">@dimen/split_divider_corner_size</item> + <item name="android:layout_height">@dimen/split_divider_corner_size</item> + </style> + <style name="DockedDividerMinimizedShadow"> <item name="android:layout_width">8dp</item> <item name="android:layout_height">match_parent</item> 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..28ff25ae0fbe 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -32,8 +32,23 @@ <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="DockedDividerTopLeftRoundCorner"> + <item name="android:layout_gravity">center_vertical|left</item> + <item name="android:background">@drawable/split_rounded_left</item> + <item name="android:layout_width">@dimen/split_divider_corner_size</item> + <item name="android:layout_height">@dimen/split_divider_corner_size</item> + </style> + + <style name="DockedDividerBottomRightRoundCorner"> + <item name="android:layout_gravity">center_vertical|right</item> + <item name="android:background">@drawable/split_rounded_right</item> + <item name="android:layout_width">@dimen/split_divider_corner_size</item> + <item name="android:layout_height">@dimen/split_divider_corner_size</item> </style> <style name="DockedDividerMinimizedShadow"> 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..df4f2383c062 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,13 @@ 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.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -38,7 +41,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 +52,17 @@ public class ShellInitImpl { private final Optional<AppPairsController> mAppPairsOptional; private final Optional<PipTouchHandler> mPipTouchHandlerOptional; private final FullscreenTaskListener mFullscreenTaskListener; + 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 +71,13 @@ public class ShellInitImpl { Optional<AppPairsController> appPairsOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, + Optional<Optional<FreeformTaskListener>> freeformTaskListenerOptional, Transitions transitions, StartingWindowController startingWindow, ShellExecutor mainExecutor) { + mDisplayController = displayController; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; mBubblesOptional = bubblesOptional; @@ -74,6 +86,7 @@ public class ShellInitImpl { mAppPairsOptional = appPairsOptional; mFullscreenTaskListener = fullscreenTaskListener; mPipTouchHandlerOptional = pipTouchHandlerOptional; + mFreeformTaskListenerOptional = freeformTaskListenerOptional.flatMap(f -> f); mTransitions = transitions; mMainExecutor = mainExecutor; mStartingWindow = startingWindow; @@ -84,7 +97,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 +123,11 @@ 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)); } @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 ba0ab6db1003..b5dffba7a0f3 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; @@ -46,6 +47,7 @@ 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 +73,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 {} @@ -486,14 +490,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); } /** @@ -572,6 +602,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; @@ -586,6 +617,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..3800b8d234f3 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; @@ -181,12 +182,13 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou // TODO: Is there more we need to do here? mSyncQueue.runInSync(t -> { - t.setLayer(dividerLeash, Integer.MAX_VALUE) + t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, mTaskInfo1.positionInParent.y) .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, mTaskInfo2.positionInParent.y) .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) + .show(dividerLeash) .show(mRootTaskLeash) .show(mTaskLeash1) .show(mTaskLeash2); @@ -212,9 +214,12 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou } mRootTaskInfo = taskInfo; - if (mSplitLayout != null - && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { - onBoundsChanged(mSplitLayout); + if (mSplitLayout != null) { + if (mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { + onLayoutChanged(mSplitLayout); + } + // updateConfiguration re-inits the dividerbar, so show it now + mSyncQueue.runInSync(t -> t.show(mSplitLayout.getDividerLeash())); } } else if (taskInfo.taskId == getTaskId1()) { mTaskInfo1 = taskInfo; @@ -295,17 +300,24 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou } @Override - public void onBoundsChanging(SplitLayout layout) { + public void onLayoutChanging(SplitLayout layout) { mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); } @Override - public void onBoundsChanged(SplitLayout layout) { + public void onLayoutChanged(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 onLayoutShifted(int offsetX, int offsetY, SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutShifted(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2); + mController.getTaskOrganizer().applyTransaction(wct); + } } 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..95b80df7fcbd 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; @@ -85,6 +84,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 +97,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 +136,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; @@ -209,7 +209,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 +218,7 @@ public class BubbleController { new BubbleDataRepository(context, launcherApps, mainExecutor), statusBarService, windowManager, windowManagerShellWrapper, launcherApps, logger, taskStackListener, organizer, positioner, displayController, mainExecutor, - mainHandler); + mainHandler, syncQueue); } /** @@ -239,7 +240,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 +264,7 @@ public class BubbleController { mSavedBubbleKeysPerUser = new SparseSetArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); mDisplayController = displayController; + mSyncQueue = syncQueue; } public void initialize() { @@ -561,6 +564,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 +579,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,7 +627,6 @@ 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(); @@ -630,10 +636,16 @@ public class BubbleController { } } - /** 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 +664,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 +765,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 +901,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; @@ -1566,13 +1569,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..b48bda3a6e48 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 @@ -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..7d7bfb2a92a3 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); @@ -632,12 +624,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 +652,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. * @@ -722,19 +696,18 @@ public class BubbleExpandedView extends LinearLayout { ? 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(() -> { float pointerY; @@ -764,6 +737,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/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..306224bd316c 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,23 +59,40 @@ 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 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; @@ -143,6 +161,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 +170,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 +255,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. */ @@ -266,46 +303,226 @@ public class BubblePositioner { } /** - * 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. */ + private float getExpandedViewYTopAligned() { final int top = getAvailableRect().top; if (showBubblesVertically()) { - return top - mPointerWidth; + return top - mPointerWidth + mExpandedViewPadding; } else { return top + mBubbleSize + mPointerMargin; } } + public float getExpandedBubblesY() { + return getAvailableRect().top + mExpandedViewPadding; + } + + /** + * 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; + } + + /** + * Returns the position of the bubble on-screen when the stack is expanded. + * + * @param index the index of the bubble in the stack. + * @param numberOfBubbles the total number of bubbles in the stack. + * @param onLeftEdge whether the stack would rest on the left edge of the screen when collapsed. + * @return the x, y position of the bubble on-screen when the stack is expanded. + */ + public PointF getExpandedBubbleXY(int index, int numberOfBubbles, boolean onLeftEdge) { + final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles); + final float expandedStackSize = (numberOfBubbles * mBubbleSize) + + ((numberOfBubbles - 1) * mSpacingBetweenBubbles); + 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 = onLeftEdge + ? left + : right; + } else { + y = mPositionRect.top + mExpandedViewPadding; + x = rowStart + positionInRow; + } + return new PointF(x, y); + } + + /** + * @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..5a51eed04e1a 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; @@ -33,11 +35,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 +108,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 +118,10 @@ public class BubbleStackView extends FrameLayout private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; + private static final int MANAGE_MENU_SCRIM_ANIM_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. @@ -195,7 +195,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. */ @@ -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, @@ -777,8 +778,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); 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 +794,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 +857,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) -> { @@ -886,8 +894,10 @@ public class BubbleStackView extends FrameLayout mExpandedAnimationController.expandFromStack(() -> { afterExpandedViewAnimation(); } /* 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); } @@ -1117,10 +1129,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 +1160,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 +1189,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 +1236,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))); } /** @@ -1255,6 +1277,7 @@ public class BubbleStackView extends FrameLayout setUpManageMenu(); setUpFlyout(); setUpDismissView(); + updateUserEdu(); mBubbleSize = mPositioner.getBubbleSize(); for (Bubble b : mBubbleData.getBubbles()) { if (b.getIconView() == null) { @@ -1535,6 +1558,7 @@ public class BubbleStackView extends FrameLayout bubble.cleanupViews(); } updatePointerPosition(); + updateExpandedView(); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } @@ -1710,6 +1734,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); @@ -1796,6 +1835,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,6 +1858,7 @@ public class BubbleStackView extends FrameLayout } beforeExpandedViewAnimation(); + showScrim(true); updateZOrder(); updateBadges(false /* setBadgeForCollapsedStack */); mBubbleContainer.setActiveController(mExpandedAnimationController); @@ -1815,37 +1869,28 @@ public class BubbleStackView extends FrameLayout 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, mBubbleContainer.getChildCount(), + mStackOnLeftOrWillBe); + 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,22 +1907,22 @@ 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); @@ -1914,6 +1959,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainerMatrix); }) .withEndActions(() -> { + mExpandedViewContainer.setAnimationMatrix(null); afterExpandedViewAnimation(); if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { @@ -1929,12 +1975,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 +2003,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 +2010,11 @@ 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, + mBubbleContainer.getChildCount(), mStackOnLeftOrWillBe); 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 +2026,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 +2054,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 +2069,6 @@ public class BubbleStackView extends FrameLayout if (previouslySelected != null) { previouslySelected.setTaskViewVisibility(false); } - - if (mPositioner.showingInTaskbar()) { - mTaskbarScrim.setVisibility(GONE); - } }) .start(); } @@ -2063,32 +2105,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), + mBubbleContainer.getChildCount(), mStackOnLeftOrWillBe); 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 +2154,7 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { mExpandedViewTemporarilyHidden = false; mIsBubbleSwitchAnimating = false; + mExpandedViewContainer.setAnimationMatrix(null); }) .start(); }, 25); @@ -2403,20 +2445,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(); }); @@ -2501,6 +2542,24 @@ public class BubbleStackView extends FrameLayout return; } + if (show) { + mManageMenuScrim.setVisibility(VISIBLE); + mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f); + } + Runnable endAction = () -> { + if (!show) { + mManageMenuScrim.setVisibility(INVISIBLE); + mManageMenuScrim.setTranslationZ(0f); + } + }; + + mManageMenuScrim.animate() + .setDuration(MANAGE_MENU_SCRIM_ANIM_DURATION) + .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. if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { @@ -2510,7 +2569,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 +2580,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,14 +2764,17 @@ 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), + mBubbleContainer.getChildCount(), mStackOnLeftOrWillBe); + mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? p.y : p.x)); mExpandedViewContainer.setTranslationX(0f); mExpandedBubble.getExpandedView().updateView( mExpandedViewContainer.getLocationOnScreen()); @@ -2792,8 +2857,13 @@ public class BubbleStackView extends FrameLayout if (index == -1) { return; } - float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index); - mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, mStackOnLeftOrWillBe); + PointF bubblePosition = mPositioner.getExpandedBubbleXY(index, + mBubbleContainer.getChildCount(), + mStackOnLeftOrWillBe); + mExpandedBubble.getExpandedView().setPointerPosition(mPositioner.showBubblesVertically() + ? bubblePosition.y + : bubblePosition.x, + mStackOnLeftOrWillBe); } /** 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..9b7eb2f1cfb3 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); 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..c32be98866cf 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 @@ -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,11 +124,10 @@ public class ExpandedAnimationController private BubblePositioner mPositioner; - public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding, + public ExpandedAnimationController(BubblePositioner positioner, Runnable onBubbleAnimatedOutAction) { mPositioner = positioner; updateResources(); - mExpandedViewPadding = expandedViewPadding; mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; mCollapsePoint = mPositioner.getDefaultStartPosition(); } @@ -208,11 +194,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 +239,22 @@ public class ExpandedAnimationController final Path path = new Path(); path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); - final float expandedY = mPositioner.showBubblesVertically() - ? getBubbleXOrYForOrientation(index) - : getExpandedY(); + boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint); + final PointF p = mPositioner.getExpandedBubbleXY(index, + mLayout.getChildCount(), + onLeft); 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.getExpandedBubblesY(); final boolean draggedOutEnough = - y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; + y > expandedY + mBubbleSizePx || y < expandedY - mBubbleSizePx; if (draggedOutEnough != mBubbleDraggedOutEnough) { updateBubblePositions(); mBubbleDraggedOutEnough = draggedOutEnough; @@ -435,9 +410,10 @@ public class ExpandedAnimationController return; } final int index = mLayout.indexOfChild(bubbleView); - + final PointF p = mPositioner.getExpandedBubbleXY(index, mLayout.getChildCount(), + mPositioner.isStackOnLeft(mCollapsePoint)); animationForChildAtIndex(index) - .position(getBubbleXOrYForOrientation(index), getExpandedY()) + .position(p.x, p.y) .withPositionStartVelocities(velX, velY) .start(() -> bubbleView.setTranslationZ(0f) /* after */); @@ -454,17 +430,13 @@ public class ExpandedAnimationController } /** - * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. + * Animates the bubbles to the y 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; + 0, (i, anim) -> anim.translationY(mPositioner.getExpandedBubblesY())) + .startAll(after); } /** Description of current animation controller state. */ @@ -522,35 +494,36 @@ 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 - // new bubble in the stacked position. - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); - float fromX = onLeft - ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR - : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; - float toX = onLeft - ? availableRect.left + mExpandedViewPadding - : availableRect.right - mBubbleSizePx - mExpandedViewPadding; - animationForChild(child) - .translationX(fromX, toX) - .start(); - updateBubblePositions(); - } } else { - child.setTranslationX(getBubbleXOrYForOrientation(index)); + boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint); + final PointF p = mPositioner.getExpandedBubbleXY(index, + mLayout.getChildCount(), + onLeft); + if (mPositioner.showBubblesVertically()) { + child.setTranslationY(p.y); + } else { + child.setTranslationX(p.x); + } 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; - animationForChild(child) - .translationY(fromY, toY) - .start(); + if (mPositioner.showBubblesVertically()) { + Rect availableRect = mPositioner.getAvailableRect(); + float fromX = onLeft + ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR + : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; + animationForChild(child) + .translationX(fromX, p.y) + .start(); + } else { + // Only animate if we're not collapsing as that animation will handle placing + // the new bubble in the stacked position. + float fromY = mPositioner.getExpandedBubblesY() - mBubbleSizePx + * ANIMATE_TRANSLATION_FACTOR; + animationForChild(child) + .translationY(fromY, p.y) + .start(); + } updateBubblePositions(); } } @@ -599,7 +572,7 @@ public class ExpandedAnimationController if (mAnimatingExpand || mAnimatingCollapse) { return; } - + boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint); for (int i = 0; i < mLayout.getChildCount(); i++) { final View bubble = mLayout.getChildAt(i); @@ -609,49 +582,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, mLayout.getChildCount(), onLeft); + 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..9a08190675b6 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 @@ -305,10 +305,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); } /** 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..9a3bdab9f418 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,28 @@ public class DisplayController { mWmService = wmService; mChangeController = new DisplayChangeController(mWmService, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); + } + + /** + * Initializes the window listener. + */ + public void initialize() { try { mWmService.registerDisplayWindowListener(mDisplayContainerListener); } 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 +98,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 +151,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 +172,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 +236,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/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index cba019a11b28..826e2f5c2f74 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,15 +19,23 @@ 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.util.TypedValue; import android.view.GestureDetector; +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.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.NonNull; @@ -44,6 +52,23 @@ 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; + // TODO(b/191269755): use the value defined in InsetsController. + private static final Interpolator RESIZE_INTERPOLATOR = Interpolators.LINEAR; + + // TODO(b/191269755): use the value defined in InsetsController. + private static final int ANIMATION_DURATION_RESIZE = 300; + + /** + * The task bar height defined in launcher. Used to determine whether to insets divider bounds + * or not. + */ + private static final int EXPANDED_TASK_BAR_HEIGHT_IN_DP = 60; + + /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */ + private final float mExpandedTaskBarHeight = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, EXPANDED_TASK_BAR_HEIGHT_IN_DP, + getResources().getDisplayMetrics()); + private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private SplitLayout mSplitLayout; @@ -58,6 +83,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,14 +129,42 @@ 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(RESIZE_INTERPOLATOR); + animator.setDuration(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); mTouchElevation = getResources().getDimensionPixelSize( 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..81cad5ac5a51 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); mTaskOrganizer = taskOrganizer; mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); + mDismissingParallaxPolicy = new DismissingParallaxPolicy(); final Resources resources = context.getResources(); mDividerWindowWidth = resources.getDimensionPixelSize( @@ -142,27 +168,61 @@ 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) { - final Rect rootBounds = configuration.windowConfiguration.getBounds(); - if (mRootBounds.equals(rootBounds)) { - return false; + boolean affectsLayout = false; + + // Make sure to render the divider bar with proper resources that matching the screen + // orientation. + final int orientation = configuration.orientation; + if (orientation != mOrientation) { + mOrientation = orientation; + mContext = mContext.createConfigurationContext(configuration); + mSplitWindowManager.setConfiguration(configuration); + affectsLayout = true; } - mContext = mContext.createConfigurationContext(configuration); - mSplitWindowManager.setConfiguration(configuration); - mRootBounds.set(rootBounds); - mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); - resetDividerPosition(); + // 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. + final int rotation = configuration.windowConfiguration.getRotation(); + final Rect rootBounds = configuration.windowConfiguration.getBounds(); + if (rotation != mRotation || !mRootBounds.equals(rootBounds)) { + mTempRect.set(mRootBounds); + mRootBounds.set(rootBounds); + mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); + initDividerPosition(mTempRect); + affectsLayout = true; + } - // Don't inflate divider bar if it is not initialized. - if (!mInitialized) { - return false; + if (mInitialized) { + release(); + init(); } - release(); - init(); - return true; + return affectsLayout; + } + + 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. */ @@ -170,7 +230,8 @@ public final class SplitLayout { 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 +244,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 +266,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,19 +290,20 @@ public final class SplitLayout { void updateDivideBounds(int position) { updateBounds(position); mSplitWindowManager.setResizingSplits(true); - mSplitLayoutHandler.onBoundsChanging(this); + mSplitLayoutHandler.onLayoutChanging(this); } void setDividePosition(int position) { mDividePosition = position; updateBounds(mDividePosition); - mSplitLayoutHandler.onBoundsChanged(this); + mSplitLayoutHandler.onLayoutChanged(this); mSplitWindowManager.setResizingSplits(false); } /** Resets divider position. */ public void resetDividerPosition() { mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; + mSplitWindowManager.setResizingSplits(false); updateBounds(mDividePosition); } @@ -232,15 +314,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 +352,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 +369,9 @@ public final class SplitLayout { @Override public void onAnimationEnd(Animator animation) { setDividePosition(to); + if (flingFinishedCallback != null) { + flingFinishedCallback.run(); + } } @Override @@ -296,42 +386,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 applyLayoutShifted(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. */ @@ -341,10 +488,18 @@ public final class SplitLayout { void onSnappedToDismiss(boolean snappedToEnd); /** Calls when the bounds is changing due to animation or dragging divider bar. */ - void onBoundsChanging(SplitLayout layout); + void onLayoutChanging(SplitLayout layout); /** Calls when the target bounds changed. */ - void onBoundsChanged(SplitLayout layout); + void onLayoutChanged(SplitLayout layout); + + /** + * Notifies when the layout shifted. So the layout handler can shift configuration + * bounds correspondingly to make sure client apps won't get configuration changed or + * relaunch. If the layout is no longer shifted, layout handler should restore shifted + * configuration bounds. + */ + void onLayoutShifted(int offsetX, int offsetY, SplitLayout layout); /** Calls when user double tapped on the divider bar. */ default void onDoubleTappedDivider() { @@ -355,6 +510,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 +664,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.onLayoutShifted(0, 0, SplitLayout.this); + } else { + mSplitLayoutHandler.onLayoutShifted(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 +690,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.onLayoutChanging(SplitLayout.this); } @Override @@ -431,7 +698,7 @@ public final class SplitLayout { SurfaceControl.Transaction t) { if (displayId != mDisplayId || cancel) return; onProgress(1.0f); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutChanging(SplitLayout.this); } @Override @@ -441,7 +708,7 @@ public final class SplitLayout { if (!controlling && mImeShown) { reset(); mSplitWindowManager.setInteractive(true); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutChanging(SplitLayout.this); } } @@ -473,24 +740,61 @@ 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) { + /** + * 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) { + if (mYOffsetForIme == 0) return false; + + 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); + t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); + + return true; } } } 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..fc7edfc4bceb 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; @@ -95,7 +96,7 @@ 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(); @@ -103,7 +104,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { } /** 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 +124,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 +170,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/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..102b90ff5d3d 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 exitTrigger); } /** @@ -352,7 +357,7 @@ public class DragAndDropPolicy { } @Override - public void exitSplitScreen() { + public void exitSplitScreen(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/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/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/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java index 7cf4fb7a811d..ff333c8c659d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java @@ -171,9 +171,22 @@ public final class OneHandedSettingsUtil { * @return true if user enabled one-handed shortcut in settings, false otherwise. */ public boolean getShortcutEnabled(ContentResolver resolver, int userId) { - final String targets = Settings.Secure.getStringForUser(resolver, + // Checks SOFTWARE_SHORTCUT_KEY + final String targetsSwKey = Settings.Secure.getStringForUser(resolver, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, userId); - return TextUtils.isEmpty(targets) ? false : targets.contains(ONE_HANDED_MODE_TARGET_NAME); + if (!TextUtils.isEmpty(targetsSwKey) && targetsSwKey.contains( + ONE_HANDED_MODE_TARGET_NAME)) { + return true; + } + + // Checks HARDWARE_SHORTCUT_KEY + final String targetsHwKey = Settings.Secure.getStringForUser(resolver, + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userId); + if (!TextUtils.isEmpty(targetsHwKey) && targetsHwKey.contains( + ONE_HANDED_MODE_TARGET_NAME)) { + return true; + } + return false; } /** 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..96867761cc7e 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; @@ -337,14 +295,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 +330,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,7 +343,7 @@ 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; } @@ -412,9 +370,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 +398,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 +441,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 +457,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 +488,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 +504,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mOnDisplayIdChangeCallback.accept(info.displayId); } - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); } else { @@ -557,6 +531,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 +544,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 +571,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 +596,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 +607,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 +636,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, null /* callback */, false /* withStartDelay */); } }, tx); - mInSwipePipToHomeTransition = false; + mPipTransitionState.setInSwipePipToHomeTransition(false); mSwipePipToHomeOverlay = null; } @@ -679,7 +660,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 +669,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 +694,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 +704,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); @@ -750,8 +731,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 +767,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 +778,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 +844,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 +860,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 +993,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 +1023,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 +1101,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 +1118,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 +1127,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 +1371,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 +1397,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 +1419,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..6fec1fbda7b0 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,9 +29,12 @@ 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.TaskInfo; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; import android.os.IBinder; import android.view.Surface; @@ -35,6 +42,7 @@ import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransactionCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -49,74 +57,218 @@ import com.android.wm.shell.transition.Transitions; */ public class PipTransition extends PipTransitionController { + 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(); 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); + 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 (info.getType() == TRANSIT_EXIT_PIP && 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; + } + + 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 onFinishResize(TaskInfo taskInfo, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, SurfaceControl.Transaction tx) { + + if (isInPipDirection(direction)) { + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); + } WindowContainerTransaction wct = new WindowContainerTransaction(); prepareFinishResizeTransaction(taskInfo, destinationBounds, direction, tx, wct); - mFinishCallback.onTransitionFinished(wct, null); + mFinishCallback.onTransitionFinished(wct, new WindowContainerTransactionCallback() { + @Override + public void onTransactionReady(int id, @NonNull SurfaceControl.Transaction t) { + t.merge(tx); + t.apply(); + } + }); finishResizeForMenu(destinationBounds); } + 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, 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 +276,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; } @@ -158,6 +312,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..dbf603ca72d9 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,6 +46,7 @@ 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<>(); @@ -55,12 +56,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 +69,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 +87,22 @@ 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. + } + public PipTransitionController(PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @@ -107,6 +112,7 @@ 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); 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 4f3ec96968b2..ac02075a49d8 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..82092ac5ac3e 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 @@ -158,14 +158,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); + } } }); 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..6ec514bd8331 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,6 +20,8 @@ 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.IRemoteTransition; import com.android.wm.shell.splitscreen.ISplitScreenListener; @@ -77,9 +79,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; + + /** + * 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/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java new file mode 100644 index 000000000000..0b763f2d05f7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineManager.java @@ -0,0 +1,127 @@ +/* + * 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.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.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.view.WindowlessWindowManager; + +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 mOutlineBounds = new Rect(); + private final Rect mTmpBounds = new Rect(); + private SurfaceControlViewHost mViewHost; + private SurfaceControl mHostLeash; + private SurfaceControl mLeash; + private int mOutlineColor; + + OutlineManager(Context context, Configuration configuration) { + super(configuration, null /* rootSurface */, null /* hostInputToken */); + mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, + null /* options */); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + b.setParent(mHostLeash); + } + + boolean drawOutlineBounds(Rect rootBounds) { + if (mLeash == null || mViewHost == null) return false; + + computeOutlineBounds(mContext, rootBounds, mTmpBounds); + if (mOutlineBounds.equals(mTmpBounds)) { + return false; + } + mOutlineBounds.set(mTmpBounds); + + ((OutlineRoot) mViewHost.getView()).updateOutlineBounds(mOutlineBounds, mOutlineColor); + final WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = rootBounds.width(); + lp.height = rootBounds.height(); + mViewHost.relayout(lp); + + return true; + } + + void inflate(SurfaceControl.Transaction t, SurfaceControl hostLeash, int color) { + if (mLeash != null || mViewHost != null) return; + + mHostLeash = hostLeash; + mOutlineColor = color; + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + final OutlineRoot rootView = (OutlineRoot) LayoutInflater.from(mContext) + .inflate(R.layout.split_outline, null); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); + 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(rootView, lp); + mLeash = getSurfaceControl(mViewHost.getWindowToken()); + t.setLayer(mLeash, Integer.MAX_VALUE); + } + + void release() { + if (mViewHost != null) { + mViewHost.release(); + } + } + + private static void computeOutlineBounds(Context context, Rect rootBounds, Rect outBounds) { + computeDisplayStableBounds(context, outBounds); + outBounds.intersect(rootBounds); + // Offset the coordinate from screen based to surface based. + outBounds.offset(-rootBounds.left, -rootBounds.top); + } + + private static void computeDisplayStableBounds(Context context, Rect outBounds) { + final WindowMetrics windowMetrics = + context.getSystemService(WindowManager.class).getMaximumWindowMetrics(); + outBounds.set(windowMetrics.getBounds()); + outBounds.inset(windowMetrics.getWindowInsets().getInsets( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout())); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java new file mode 100644 index 000000000000..71d48eeca71d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineRoot.java @@ -0,0 +1,62 @@ +/* + * 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 android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** Root layout for holding split outline. */ +public class OutlineRoot extends FrameLayout { + public OutlineRoot(@NonNull Context context) { + super(context); + } + + public OutlineRoot(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + private OutlineView mOutlineView; + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mOutlineView = findViewById(R.id.split_outline); + } + + void updateOutlineBounds(Rect bounds, int color) { + mOutlineView.updateOutlineBounds(bounds, color); + } +} 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..ea66180e3dd2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OutlineView.java @@ -0,0 +1,76 @@ +/* + * 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 android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.AttributeSet; +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 Rect mBounds = new Rect(); + + public OutlineView(@NonNull Context context) { + super(context); + } + + public OutlineView(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(getResources() + .getDimension(R.dimen.accessibility_focus_highlight_stroke_width)); + } + + void updateOutlineBounds(Rect bounds, int color) { + if (mBounds.equals(bounds) && mPaint.getColor() == color) return; + mBounds.set(bounds); + mPaint.setColor(color); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mBounds.isEmpty()) return; + final Path path = new Region(mBounds).getBoundaryPath(); + canvas.drawPath(path, mPaint); + } +} 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..2b19bb965fed 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,7 +16,10 @@ package com.android.wm.shell.splitscreen; +import android.annotation.CallSuper; import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Color; import android.graphics.Rect; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -28,15 +31,19 @@ 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 { 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); + mContext = context; } void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, @@ -44,7 +51,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 +76,34 @@ class SideStage extends StageTaskListener { wct.reparent(task.token, newParent, false /* onTop */); return true; } + + void enableOutline(boolean enable) { + if (enable) { + if (mOutlineManager == null && mRootTaskInfo != null) { + mOutlineManager = new OutlineManager(mContext, mRootTaskInfo.configuration); + mSyncQueue.runInSync(t -> mOutlineManager.inflate(t, mRootLeash, Color.YELLOW)); + updateOutlineBounds(); + } + } else { + if (mOutlineManager != null) { + mOutlineManager.release(); + mOutlineManager = null; + } + } + } + + private void updateOutlineBounds() { + if (mOutlineManager == null || mRootTaskInfo == null || !mRootTaskInfo.isVisible) return; + mOutlineManager.drawOutlineBounds( + mRootTaskInfo.configuration.windowConfiguration.getBounds()); + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + super.onTaskInfoChanged(taskInfo); + if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId) { + updateOutlineBounds(); + } + } } 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..437b52a31ee4 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 @@ -17,14 +17,12 @@ package com.android.wm.shell.splitscreen; 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 +36,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.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.IRemoteTransition; +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,10 +64,12 @@ 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.concurrent.Executor; /** * Class manages split-screen multitasking mode and implements the main interface @@ -76,8 +87,10 @@ 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 StageCoordinator mStageCoordinator; @@ -85,6 +98,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, SyncTransactionQueue syncQueue, Context context, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellExecutor mainExecutor, DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool) { mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; @@ -92,8 +106,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mTransitions = transitions; mTransactionPool = transactionPool; + mLogger = new SplitscreenEventLogger(); } public SplitScreen asSplitScreen() { @@ -114,8 +130,8 @@ 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); } } @@ -140,8 +156,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 +173,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 exitReason) { + mStageCoordinator.exitSplitScreen(exitReason); + } + + public void onKeyguardOccludedChanged(boolean occluded) { + mStageCoordinator.onKeyguardOccludedChanged(occluded); + } + + public void onKeyguardVisibilityChanged(boolean showing) { + mStageCoordinator.onKeyguardVisibilityChanged(showing); } public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { @@ -175,7 +203,7 @@ 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); @@ -187,7 +215,7 @@ 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 = @@ -202,64 +230,96 @@ 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) { + final boolean wasInSplit = isSplitScreenVisible(); + + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + boolean cancelled = apps == null || apps.length == 0; + mStageCoordinator.updateSurfaceBounds(null /* layout */, t); + if (cancelled) { + if (!wasInSplit) { + final WindowContainerTransaction undoWct = new WindowContainerTransaction(); + mStageCoordinator.prepareExitSplitScreen(STAGE_TYPE_MAIN, undoWct); + mSyncQueue.queue(undoWct); + mSyncQueue.runInSync(undoT -> { + // looks weird, but we want undoT to execute after t but still want the + // rest of the syncQueue runnables to aggregate. + t.merge(undoT); + undoT.merge(t); + }); + return; } } 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(); + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } } - 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(); + RemoteAnimationTarget divider = mStageCoordinator.getDividerBarLegacyTarget(); + if (divider.leash != null) { + t.show(divider.leash); } - if (options == null) { - options = new Bundle(); + t.apply(); + if (cancelled) return; + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); } - mStageCoordinator.updateActivityOptions(options, position); - break; } - 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()}; + } - 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 +335,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 +376,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); + }); + } } /** @@ -380,7 +514,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, public void exitSplitScreen() { executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", (controller) -> { - controller.exitSplitScreen(); + controller.exitSplitScreen( + FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); }); } @@ -417,6 +552,16 @@ 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, @@ -444,5 +589,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..69d0be6abc0b 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 @@ -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, 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..9e6edd2a5381 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 @@ -20,11 +20,19 @@ 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 +43,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,11 +51,23 @@ 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; @@ -59,10 +80,12 @@ 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; @@ -114,14 +137,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. @@ -136,13 +165,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; mRootTDAOrganizer = rootTDAOrganizer; mTaskOrganizer = taskOrganizer; + mLogger = logger; mMainStage = new MainStage( mTaskOrganizer, mDisplayId, @@ -150,13 +181,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSyncQueue, mSurfaceSession); mSideStage = new SideStage( + mContext, mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue, mSurfaceSession); mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; 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 +203,9 @@ 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) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -175,10 +214,12 @@ 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); + mLogger = logger; transitions.addHandler(this); } @@ -194,7 +235,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean moveToSideStage(ActivityManager.RunningTaskInfo task, @SplitPosition int sideStagePosition) { final WindowContainerTransaction wct = new WindowContainerTransaction(); - setSideStagePosition(sideStagePosition); + setSideStagePosition(sideStagePosition, wct); mMainStage.activate(getMainStageBounds(), wct); mSideStage.addTask(task, getSideStageBounds(), wct); mTaskOrganizer.applyTransaction(wct); @@ -215,6 +256,10 @@ 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, @@ -222,7 +267,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, 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. @@ -241,6 +286,138 @@ 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); + 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 IRemoteTransition 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; + } + @SplitLayout.SplitPosition int getSideStagePosition() { return mSideStagePosition; @@ -252,18 +429,24 @@ 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) { + 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) { + // onBoundsChanged builds/applies a wct with the contents of updateWindowBounds. + onLayoutChanged(mSplitLayout); + } else { + updateWindowBounds(mSplitLayout, wct); + } } } @@ -275,24 +458,49 @@ 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 exitSplitScreen(int exitReason) { + exitSplitScreen(null /* childrenToTop */, exitReason); } void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mExitSplitScreenOnHide = exitSplitScreenOnHide; } - private void exitSplitScreen(StageTaskListener childrenToTop) { + private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); mMainStage.deactivate(wct, childrenToTop == mMainStage); mTaskOrganizer.applyTransaction(wct); // Reset divider position. 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 +517,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 +545,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 +554,26 @@ 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); + } + } + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); @@ -395,6 +613,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else { mSplitLayout.release(); } + sendSplitVisibilityChanged(); } private void onStageVisibilityChanged(StageListenerImpl stageListener) { @@ -403,22 +622,37 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // 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(); + if (!mainStageVisible && !sideStageVisible) { + 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(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); } - if (mainStageVisible) { + // When both stage's visibility changed to visible, main stage might receives visibility + // changed before side stage if it has higher z-order than side stage. Make sure we only + // update main stage's windowing mode with the visibility changed of side stage to prevent + // stacking multiple windowing mode transactions which result to flicker issue. + if (mainStageVisible && stageListener == mSideStageListener) { 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 { + } else if (!mSideStage.mRootTaskInfo.isSleeping) { // We want the main stage configuration to be fullscreen when the side stage isn't // visible. + // We should not do it when side stage are not visible due to sleeping display too. mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct); } // TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable. @@ -426,22 +660,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } 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); - } - } - if (sideStageVisible) { final Rect sideStageBounds = getSideStageBounds(); t.show(sideStageLeash) @@ -468,31 +689,54 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else { t.hide(mainStageLeash); } + + applyDividerVisibility(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 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); + 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); 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 +755,52 @@ 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 onLayoutChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); } @Override - public void onBoundsChanging(SplitLayout layout) { + public void onLayoutChanged(SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateWindowBounds(layout, wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); + } + + /** + * 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,6 +819,18 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override + public void onLayoutShifted(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.applyLayoutShifted(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo, + bottomRightStage.mRootTaskInfo); + mTaskOrganizer.applyTransaction(wct); + } + + @Override public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { mDisplayAreaInfo = displayAreaInfo; if (mSplitLayout == null) { @@ -568,6 +838,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayAreaInfo.configuration, this, b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b), mDisplayImeController, mTaskOrganizer); + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); } } @@ -580,8 +851,21 @@ 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()) { + onLayoutChanged(mSplitLayout); + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + } + } + + 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 +956,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 +1002,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 +1039,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 +1140,22 @@ 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); + } + @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; @@ -884,6 +1180,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 +1249,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onNoLongerSupportMultiWindow() { if (mMainStage.isActive()) { - StageCoordinator.this.exitSplitScreen(); + StageCoordinator.this.exitSplitScreen( + 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..3512a0c3727b 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 @@ -71,8 +71,8 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } private final StageListenerCallbacks mCallbacks; - private final SyncTransactionQueue mSyncQueue; private final SurfaceSession mSurfaceSession; + protected final SyncTransactionQueue mSyncQueue; protected ActivityManager.RunningTaskInfo mRootTaskInfo; protected SurfaceControl mRootLeash; @@ -97,6 +97,29 @@ 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.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) { 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..22865988e880 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 @@ -360,7 +360,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"); 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 fc7c86d669cb..6c60bad31dba 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 @@ -52,6 +52,7 @@ 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; @@ -115,6 +116,8 @@ public class StartingSurfaceDrawer { @VisibleForTesting final SplashscreenContentDrawer mSplashscreenContentDrawer; private Choreographer mChoreographer; + private final WindowManagerGlobal mWindowManagerGlobal; + private StartingSurface.SysuiProxy mSysuiProxy; /** * @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,21 +142,8 @@ 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) { @@ -160,6 +152,11 @@ public class StartingSurfaceDrawer { : 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. * @@ -186,13 +183,11 @@ public class StartingSurfaceDrawer { + " 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; if (theme != context.getThemeResId()) { try { context = context.createPackageContextAsUser(activityInfo.packageName, @@ -328,12 +323,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); - 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, @@ -508,12 +504,14 @@ public class StartingSurfaceDrawer { viewHost.getView().post(viewHost::release); } - 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 +519,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; } } @@ -584,13 +582,13 @@ public class StartingSurfaceDrawer { } 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 e84d498a9258..a5c47c41180e 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 @@ -217,6 +217,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 6052d3dee891..7d011e6521a2 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 @@ -72,6 +72,7 @@ import android.view.IWindowSession; import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.InsetsVisibilities; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.View; @@ -205,7 +206,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; @@ -225,13 +226,14 @@ public class TaskSnapshotWindow { delayRemovalTime, topWindowInsetsState, clearWindowHandler, splashScreenExecutor); final Window window = snapshotSurface.mWindow; - final InsetsState mTmpInsetsState = new InsetsState(); + final InsetsState tmpInsetsState = new InsetsState(); + final InsetsVisibilities tmpRequestedVisibilities = new InsetsVisibilities(); 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); + tmpRequestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); @@ -244,8 +246,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(); 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..4ba6acaba025 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,30 +16,55 @@ 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 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; 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; @@ -48,9 +73,12 @@ import android.window.TransitionRequestInfo; 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; @@ -61,33 +89,173 @@ 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 rotating, check if it should be seamless."); + boolean checkedDisplayLayout = 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) { + // 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; + } + } + } + } + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); + return true; + } + + /** + * 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); @@ -97,19 +265,42 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; + + 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 (info.getType() == TRANSIT_CHANGE && change.getMode() == TRANSIT_CHANGE + && (change.getEndRotation() != change.getStartRotation()) + && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { + 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; + } + } + 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 +308,17 @@ 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(); // run finish now in-case there are no animations onAnimFinish.run(); return true; @@ -141,87 +337,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 if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { + if (isOpeningType) { + a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter); } 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 { - 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 +474,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 +500,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/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..6bd805323aa3 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 @@ -57,7 +57,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 +71,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.startAnimation(transition, info, startTransaction, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); if (mRemote.asBinder() != null) { @@ -102,7 +108,8 @@ 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 */)); } 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..bda884cd80d9 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 @@ -56,14 +56,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { private final ArrayList<Pair<TransitionFilter, IRemoteTransition>> 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; @@ -71,7 +64,9 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { void addFiltered(TransitionFilter filter, IRemoteTransition remote) { try { - remote.asBinder().linkToDeath(mTransitionDeathRecipient, 0 /* flags */); + RemoteDeathHandler handler = new RemoteDeathHandler(remote.asBinder()); + remote.asBinder().linkToDeath(handler, 0 /* flags */); + mDeathHandlers.put(remote.asBinder(), handler); } catch (RemoteException e) { Slog.e(TAG, "Failed to link to death"); return; @@ -88,7 +83,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { } } if (removed) { - remote.asBinder().unlinkToDeath(mTransitionDeathRecipient, 0 /* flags */); + RemoteDeathHandler handler = mDeathHandlers.remove(remote.asBinder()); + remote.asBinder().unlinkToDeath(handler, 0 /* flags */); } } @@ -99,7 +95,8 @@ 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); if (pendingRemote == null) { @@ -110,6 +107,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); @@ -132,11 +130,15 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { }; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { if (remote.asBinder() != null) { remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } mMainExecutor.execute(() -> { + if (sct != null) { + finishTransaction.merge(sct); + } mRequestedRemotes.remove(transition); finishCallback.onTransitionFinished(wct, null /* wctCB */); }); @@ -146,7 +148,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { if (remote.asBinder() != null) { remote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - remote.startAnimation(transition, info, t, cb); + remote.startAnimation(transition, info, startTransaction, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); if (remote.asBinder() != null) { @@ -170,7 +172,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { 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 " @@ -200,4 +203,25 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { + " for %s: %s", transition, remote); return new WindowContainerTransaction(); } + + /** NOTE: binder deaths can alter the filter order */ + private class RemoteDeathHandler implements IBinder.DeathRecipient { + private final IBinder mRemote; + + RemoteDeathHandler(IBinder remote) { + mRemote = remote; + } + + @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); + } + } + }); + } + } } 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..ada2ed27c114 --- /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.setPosition(mAnimLeash, 0, 0); + t.setAlpha(mAnimLeash, 1); + t.show(mAnimLeash); + + t.setLayer(mScreenshotLayer, SCREEN_FREEZE_LAYER_BASE); + 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/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 60707ccdca30..8d21ce25bcd0 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; @@ -54,6 +55,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 +79,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 +102,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); @@ -218,7 +231,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 +395,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 +429,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 +449,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 +488,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 +524,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 +602,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); /** 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/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/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index c5b5b91d570b..b36468b7e9a5 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 @@ -16,97 +16,100 @@ package com.android.wm.shell.flicker +import android.content.ComponentName 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 -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: ComponentName +) { 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: ComponentName ) { 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: ComponentName ) { 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: ComponentName ) { 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..ff1a6e6d9d90 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 android.content.ComponentName + 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 = ComponentName("", "AppPairSplitDivider#") +val DOCKED_STACK_DIVIDER_COMPONENT = ComponentName("", "DockedStackDivider#")
\ No newline at end of file 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..19374ed04be5 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) + isVisible(nonResizeableApp.component) + isInvisible(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..46ee89295a4e 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) + isVisible(primaryApp.component) + isVisible(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..f7ced71afe8a 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) + isVisible(nonResizeableApp.component) + isVisible(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..3debdd3276e4 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) + isInvisible(primaryApp.component) + isInvisible(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..cdf89a57fde8 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 @@ -30,14 +30,14 @@ 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.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.BaseAppHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow @@ -154,26 +154,26 @@ 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 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..3e782e608c86 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) + isVisible(primaryApp.component) + .isVisible(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..ee28c7aa6beb 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) + isVisible(primaryApp.component) + isVisible(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/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 5b8cfb81016a..5a438af0b1f1 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 @@ -19,6 +19,7 @@ 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 class AppPairsHelper( @@ -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..f15044ef37af 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 @@ -20,6 +20,7 @@ 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 @@ -60,6 +61,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/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index cac46fe676b3..086e8b792e0e 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 @@ -61,7 +61,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 +78,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/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index f4dd7decb1b7..d4b4e5daf7cb 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 @@ -62,7 +62,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 +84,7 @@ 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) } fun clickStartMediaSessionButton() { @@ -137,7 +137,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } // Wait for animation to complete. - wmHelper.waitFor { !it.wmState.hasPipWindow() } + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } wmHelper.waitForHomeActivityVisible() } @@ -167,7 +167,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val windowRect = windowRegion.bounds uiDevice.click(windowRect.centerX(), windowRect.centerY()) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - wmHelper.waitFor { !it.wmState.hasPipWindow() } + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() } 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..2d996ca1d6f7 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation import android.content.ComponentName +import android.content.res.Resources import com.android.wm.shell.flicker.testapp.Components class SplitScreenHelper( @@ -30,6 +31,11 @@ class SplitScreenHelper( 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, 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..508e93988aa6 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 @@ -16,22 +16,24 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName 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.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.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper 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 @@ -60,16 +62,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT, + splitScreenApp.component, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT, 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 +79,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) + isVisible(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..12f3909b6c34 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -25,7 +26,7 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory 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.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -61,24 +62,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT, + splitScreenApp.component) @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) + isVisible(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..ac85c4857c76 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,17 +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.Group1 -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.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.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper 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 @@ -62,22 +62,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, + secondaryApp.component, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @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 +85,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, ignoreActivity = true) + .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..964af2341439 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -26,7 +27,7 @@ import com.android.server.wm.flicker.annotation.Group1 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.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 @@ -70,12 +71,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT, + nonResizeableApp.component, + splitScreenApp.component) @Before override fun setup() { @@ -91,7 +92,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..1b8afa668802 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -26,7 +27,7 @@ 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.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 +68,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT, + nonResizeableApp.component, + splitScreenApp.component) @Before override fun setup() { @@ -88,16 +89,21 @@ class EnterSplitScreenSupportNonResizable( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) + isVisible(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..247965f8071d 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,8 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.platform.test.annotations.Presubmit +import android.content.ComponentName +import android.platform.test.annotations.Postsubmit import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -24,15 +25,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.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER +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 +66,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + splitScreenApp.component, secondaryApp.component, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) - @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..af99fc4af1a0 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 @@ -16,6 +16,8 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest @@ -24,15 +26,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.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +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 +71,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + splitScreenApp.component, secondaryApp.component, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) - @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 navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Postsubmit + @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/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt index d0a64b3774c7..95e4085db4eb 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,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.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.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 +69,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<ComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + nonResizeableApp.component, splitScreenApp.component, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @Before override fun setup() { @@ -92,44 +89,110 @@ 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, ignoreActivity = 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 + .isAppWindowInvisible(splitScreenApp.component, ignoreActivity = true) + } + } + /** + * 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, + ignoreActivity = true, 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, ignoreActivity = true) + } + } + /** + * 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 { + this.isVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @Presubmit @Test fun onlyNonResizableAppWindowIsVisibleAtEnd() { testSpec.assertWmEnd { - isInvisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isInvisible(splitScreenApp.component) + isVisible(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..65346aa8ea5d 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,13 +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.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.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 +69,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<ComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + nonResizeableApp.component, splitScreenApp.component, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @Before override fun setup() { @@ -90,27 +89,60 @@ 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, + ignoreActivity = true, 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, ignoreActivity = true) + } + } @Presubmit @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppsWindowsAreVisibleAtEnd() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isVisible(splitScreenApp.component) + isVisible(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..547341a14cdd 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,8 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,16 +25,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.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 +71,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<ComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @Before override fun setup() { @@ -93,37 +91,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, ignoreActivity = 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 + .isAppWindowInvisible(splitScreenApp.component, ignoreActivity = true) + } + } - @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) + isInvisible(splitScreenApp.component) + isVisible(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..3f86658297fe 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,14 +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.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.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 +70,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<ComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @Before override fun setup() { @@ -91,27 +90,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, ignoreActivity = true) + .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) + isVisible(splitScreenApp.component) + isVisible(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..7b4b71b41967 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,10 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName +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,20 +27,18 @@ 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.flicker.statusBarWindowIsVisible import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible import com.android.wm.shell.flicker.helpers.SimpleAppHelper @@ -62,8 +60,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,25 +86,25 @@ class LegacySplitScreenToLauncher( } } - override val ignoredWindows: List<String> - get() = listOf(launcherPackageName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @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(testSpec.config.endRotation) @Presubmit @Test @@ -122,19 +118,39 @@ class LegacySplitScreenToLauncher( @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..311769313a7a 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.flicker.legacysplitscreen import android.app.Instrumentation +import android.content.ComponentName import android.content.Context import android.support.test.launcherhelper.LauncherStrategyFactory import android.view.Surface @@ -32,10 +33,13 @@ 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.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 +50,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 = ComponentName("", + 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 +79,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<ComponentName> = listOf( + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { configuration -> @@ -138,9 +148,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 = ComponentName("", + "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2") + internal val LETTERBOX_COMPONENT = ComponentName("", "Letterbox") + internal val TOAST_COMPONENT = ComponentName("", "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..ec0c73a58846 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.content.ComponentName import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest @@ -24,14 +25,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.flicker.statusBarLayerIsVisible import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.appPairsDividerBecomesVisible import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -62,22 +60,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<ComponentName> + get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, + WindowManagerStateHelper.SPLASH_SCREEN_COMPONENT, + WindowManagerStateHelper.SNAPSHOT_COMPONENT) @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(testSpec.config.startRotation) @Presubmit @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Presubmit @Test @@ -85,12 +89,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..d7f71a83ba7e 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 @@ -28,23 +28,23 @@ 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.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) } } @@ -118,18 +118,18 @@ class ResizeLegacySplitScreen( @Test fun bottomAppWindowIsAlwaysVisible() { testSpec.assertWm { - this.showsAppWindow(sImeActivity) + this.isAppWindowVisible(Components.ImeActivity.COMPONENT) } } @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(testSpec.config.endRotation) @Test fun navBarLayerRotatesAndScales() = @@ -142,21 +142,21 @@ class ResizeLegacySplitScreen( @Test fun topAppLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(sSimpleActivity) + this.isVisible(Components.SimpleActivity.COMPONENT) } } @Test fun bottomAppLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(sImeActivity) + this.isVisible(Components.ImeActivity.COMPONENT) } } @Test fun dividerLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } @@ -166,7 +166,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 +174,8 @@ class ResizeLegacySplitScreen( dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) - visibleRegion("SimpleActivity").coversExactly(topAppBounds) - visibleRegion("ImeActivity").coversExactly(bottomAppBounds) + visibleRegion(Components.SimpleActivity.COMPONENT).coversExactly(topAppBounds) + visibleRegion(Components.ImeActivity.COMPONENT).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,8 @@ class ResizeLegacySplitScreen( displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) - visibleRegion(sSimpleActivity).coversExactly(topAppBounds) - visibleRegion(sImeActivity).coversExactly(bottomAppBounds) + visibleRegion(Components.SimpleActivity.COMPONENT).coversExactly(topAppBounds) + visibleRegion(Components.ImeActivity.COMPONENT).coversExactly(bottomAppBounds) } } @@ -207,8 +207,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..8a2b55b6fce0 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,17 @@ 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,21 +65,21 @@ 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) - @FlakyTest(bugId = 169271943) + @Presubmit @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, @@ -88,16 +87,26 @@ class RotateOneLaunchedAppAndEnterSplitScreen( @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..b3251573c942 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,17 @@ 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 +65,45 @@ 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) - @FlakyTest(bugId = 169271943) + @Presubmit @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales( testSpec.config.startRotation, testSpec.config.endRotation) @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..2be693631b26 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,24 @@ 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 +67,66 @@ 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) - @FlakyTest(bugId = 169271943) + @Presubmit @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales( testSpec.config.startRotation, testSpec.config.endRotation) @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, ignoreActivity = true) + .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 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..5782f145c00f 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,19 @@ 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,27 +73,27 @@ 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) - @FlakyTest(bugId = 169271943) + @Presubmit @Test fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, @@ -102,16 +101,31 @@ class RotateTwoLaunchedAppInSplitScreenMode( @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..443204c245db 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. @@ -16,4 +16,4 @@ 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/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt index 00e50e7fe3b5..39e89fbd9b71 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/EnterExitPipTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -62,18 +63,21 @@ class EnterExitPipTest( @Test fun pipAppRemainInsideVisibleBounds() { testSpec.assertWm { - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } - @Presubmit + @Postsubmit @Test fun showBothAppWindowsThenHidePip() { testSpec.assertWm { - showsAppWindow(testApp.defaultWindowName) - .showsAppWindowOnTop(pipApp.defaultWindowName) + // 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, ignoreActivity = true) + .isAppWindowOnTop(pipApp.component) .then() - .hidesAppWindow(testApp.defaultWindowName) + .isAppWindowInvisible(testApp.component) } } @@ -81,10 +85,10 @@ class EnterExitPipTest( @Test fun showBothAppLayersThenHidePip() { testSpec.assertLayers { - isVisible(testApp.defaultWindowName) - .isVisible(pipApp.defaultWindowName) + isVisible(testApp.component) + .isVisible(pipApp.component) .then() - .isInvisible(testApp.defaultWindowName) + .isInvisible(testApp.component) } } @@ -92,8 +96,8 @@ class EnterExitPipTest( @Test fun testAppCoversFullScreenWithPipOnDisplay() { testSpec.assertLayersStart { - visibleRegion(testApp.defaultWindowName).coversExactly(displayBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds) + visibleRegion(testApp.component).coversExactly(displayBounds) + visibleRegion(pipApp.component).coversAtMost(displayBounds) } } @@ -101,7 +105,7 @@ class EnterExitPipTest( @Test fun pipAppCoversFullScreen() { testSpec.assertLayersEnd { - visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds) + visibleRegion(pipApp.component).coversExactly(displayBounds) } } 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..0f0a4abe30ef 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 @@ -44,30 +44,24 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true, stringExtras = emptyMap()) { transitions { - pipApp.clickEnterPipButton() + pipApp.clickEnterPipButton(wmHelper) pipApp.expandPipWindow(wmHelper) } } - @FlakyTest - @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() - } - @Presubmit @Test fun pipAppWindowAlwaysVisible() { testSpec.assertWm { - this.showsAppWindow(pipApp.defaultWindowName) + this.isAppWindowVisible(pipApp.component) } } - @FlakyTest + @Presubmit @Test - fun pipLayerBecomesVisible() { + fun pipAppLayerAlwaysVisible() { testSpec.assertLayers { - this.isVisible(pipApp.windowName) + this.isVisible(pipApp.component) } } 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..67ad322f19f6 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 @@ -92,15 +92,13 @@ class EnterPipToOtherOrientationTest( @FlakyTest @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() - } + override fun entireScreenCovered() = super.entireScreenCovered() @Presubmit @Test fun pipAppWindowIsAlwaysOnTop() { testSpec.assertWm { - showsAppWindowOnTop(pipApp.defaultWindowName) + isAppWindowOnTop(pipApp.component) } } @@ -108,7 +106,7 @@ class EnterPipToOtherOrientationTest( @Test fun pipAppHidesTestApp() { testSpec.assertWmStart { - isInvisible(testApp.defaultWindowName) + isInvisible(testApp.component) } } @@ -116,7 +114,7 @@ class EnterPipToOtherOrientationTest( @Test fun testAppWindowIsVisible() { testSpec.assertWmEnd { - isVisible(testApp.defaultWindowName) + isVisible(testApp.component) } } @@ -124,8 +122,8 @@ class EnterPipToOtherOrientationTest( @Test fun pipAppLayerHidesTestApp() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversExactly(startingBounds) - isInvisible(testApp.defaultWindowName) + visibleRegion(pipApp.component).coversExactly(startingBounds) + isInvisible(testApp.component) } } @@ -133,7 +131,7 @@ class EnterPipToOtherOrientationTest( @Test fun testAppLayerCoversFullScreen() { testSpec.assertLayersEnd { - visibleRegion(testApp.defaultWindowName).coversExactly(endingBounds) + visibleRegion(testApp.component).coversExactly(endingBounds) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt deleted file mode 100644 index 0037059e2c51..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt +++ /dev/null @@ -1,29 +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 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 - -/** - * Checks that an activity [activity] is in PIP mode - */ -fun WindowManagerState.isInPipMode(activity: ComponentName): Boolean { - val windowName = activity.toWindowName() - return isInPipMode(windowName) -} 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/PipCloseTransition.kt index eae7e973711c..28b1028d41ca 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/PipCloseTransition.kt @@ -21,8 +21,8 @@ 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 @@ -47,9 +47,9 @@ abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransitio @Test open fun pipWindowBecomesInvisible() { testSpec.assertWm { - this.showsAppWindow(PIP_WINDOW_TITLE) + this.invoke("hasPipWindow") { it.isPinned(pipApp.component) } .then() - .hidesAppWindow(PIP_WINDOW_TITLE) + .isAppWindowInvisible(pipApp.component) } } @@ -57,15 +57,21 @@ abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransitio @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) } } @FlakyTest(bugId = 151179149) @Test - open fun focusChanges() = testSpec.focusChanges(pipApp.launcherName, "NexusLauncherActivity") + open fun focusChanges() { + testSpec.assertEventLog { + this.focusChanges(pipApp.launcherName, "NexusLauncherActivity") + } + } companion object { @Parameterized.Parameters(name = "{0}") 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/pip/PipCloseWithDismissButtonTest.kt index cf84a2c696d0..1c5d77f68f43 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/pip/PipCloseWithDismissButtonTest.kt @@ -48,13 +48,9 @@ class PipCloseWithDismissButtonTest(testSpec: FlickerTestParameter) : PipCloseTr @FlakyTest @Test - override fun pipLayerBecomesInvisible() { - super.pipLayerBecomesInvisible() - } + override fun pipLayerBecomesInvisible() = super.pipLayerBecomesInvisible() @FlakyTest @Test - override fun pipWindowBecomesInvisible() { - super.pipWindowBecomesInvisible() - } + 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/pip/PipCloseWithSwipeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt index 524a1b404591..356ec94b97e3 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/PipCloseWithSwipeTest.kt @@ -56,19 +56,19 @@ class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition @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 @@ -85,7 +85,7 @@ class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition @Presubmit @Test - override fun noUncoveredRegions() = super.noUncoveredRegions() + override fun entireScreenCovered() = super.entireScreenCovered() @Presubmit @Test 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..5719413aff25 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 @@ -27,7 +27,7 @@ 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.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.helpers.ImeAppHelper import org.junit.FixMethodOrder import org.junit.Test @@ -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(WindowManagerStateHelper.IME_COMPONENT, 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..086165289d2d 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 @@ -27,11 +27,16 @@ import com.android.server.wm.flicker.annotation.Group3 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 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) + isVisible(testApp.component) + isVisible(imeApp.component) + noWindowsOverlap(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..45cb152e5b0d 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 @@ -26,12 +26,14 @@ 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.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.server.wm.traces.common.Region +import com.android.server.wm.traces.parser.minus import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.FixMethodOrder import org.junit.Test @@ -73,9 +75,9 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) } } - @FlakyTest(bugId = 185400889) + @Presubmit @Test - override fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation, + override fun entireScreenCovered() = testSpec.entireScreenCovered(testSpec.config.startRotation, testSpec.config.endRotation, allStates = false) @FlakyTest @@ -90,21 +92,27 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, testSpec.config.endRotation) - @FlakyTest(bugId = 185400889) + @Presubmit @Test fun appLayerRotates_StartingBounds() { testSpec.assertLayersStart { - visibleRegion(fixedApp.defaultWindowName).coversExactly(startingBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + val pipRegion = visibleRegion(pipApp.component).region + val expectedWithoutPip = Region(startingBounds.bounds.left, startingBounds.bounds.top, + startingBounds.bounds.right, startingBounds.bounds.bottom).minus(pipRegion) + visibleRegion(fixedApp.component).coversExactly(expectedWithoutPip) + visibleRegion(pipApp.component).coversAtMost(startingBounds) } } - @FlakyTest(bugId = 185400889) + @Presubmit @Test fun appLayerRotates_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(fixedApp.defaultWindowName).coversExactly(endingBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(endingBounds) + val pipRegion = visibleRegion(pipApp.component).region + val expectedWithoutPip = Region(endingBounds.bounds.left, endingBounds.bounds.top, + endingBounds.bounds.right, endingBounds.bounds.bottom).minus(pipRegion) + visibleRegion(fixedApp.component).coversExactly(expectedWithoutPip) + visibleRegion(pipApp.component).coversAtMost(endingBounds) } } 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/PipShelfHeightTest.kt index 1294ac93f647..914bc8b4d1c9 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/PipShelfHeightTest.kt @@ -63,13 +63,13 @@ class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpe @Presubmit @Test - fun pipAlwaysVisible() = testSpec.assertWm { this.showsAppWindow(pipApp.windowName) } + fun pipAlwaysVisible() = testSpec.assertWm { this.isAppWindowVisible(pipApp.component) } @Presubmit @Test fun pipLayerInsideDisplay() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds) + visibleRegion(pipApp.component).coversAtMost(displayBounds) } } 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 index 55e5c4128967..5abcf39f3fbd 100644 --- 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 @@ -22,9 +22,9 @@ 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 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 @@ -64,9 +64,11 @@ class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Test fun appReplacesPipWindow() { testSpec.assertWm { - this.showsAppWindow(PIP_WINDOW_TITLE) + this.invoke("hasPipWindow") { it.isPinned(pipApp.component) } + .isAppWindowOnTop(pipApp.component) .then() - .showsAppWindowOnTop(pipApp.launcherName) + .invoke("hasNotPipWindow") { it.isNotPinned(pipApp.component) } + .isAppWindowOnTop(pipApp.component) } } @@ -74,9 +76,11 @@ class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Test fun appReplacesPipLayer() { testSpec.assertLayers { - this.isVisible(PIP_WINDOW_TITLE) + this.isVisible(pipApp.component) + .isVisible(LAUNCHER_COMPONENT) .then() - .isVisible(pipApp.launcherName) + .isVisible(pipApp.component) + .isInvisible(LAUNCHER_COMPONENT) } } @@ -84,22 +88,26 @@ class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @Test fun testAppCoversFullScreen() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds) + visibleRegion(pipApp.component).coversExactly(displayBounds) } } @FlakyTest(bugId = 151179149) @Test - fun focusChanges() = testSpec.focusChanges("NexusLauncherActivity", - pipApp.launcherName, "NexusLauncherActivity") + fun focusChanges() { + testSpec.assertEventLog { + this.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) + .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..ca80d1837ea8 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,19 +161,19 @@ 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 @@ -188,6 +187,6 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { @Presubmit @Test - open fun noUncoveredRegions() = - testSpec.noUncoveredRegions(testSpec.config.startRotation, Surface.ROTATION_0) + open fun entireScreenCovered() = + testSpec.entireScreenCovered(testSpec.config.startRotation, Surface.ROTATION_0) }
\ 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..e7b61970cbeb 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,7 +16,6 @@ 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 @@ -83,54 +82,70 @@ 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 { 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/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/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 3e3195fe8dc5..b0312e6d6f3c 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 @@ -869,6 +869,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 +931,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 +954,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); 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..9732a8890e0e 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.animation; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -41,7 +42,6 @@ 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 +49,26 @@ 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; @SuppressLint("VisibleForTests") @Before public void setUp() throws Exception { super.setUp(); - BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class)); - positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, + 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, + mExpandedController = new ExpandedAnimationController(mPositioner, mOnBubbleAnimatedOutAction); + spyOn(mExpandedController); addOneMoreThanBubbleLimitBubbles(); mLayout.setActiveController(mExpandedController); @@ -141,13 +141,16 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC /** Check that children are in the correct positions for being expanded. */ private void testBubblesInCorrectExpandedPositions() { + boolean onLeft = mPositioner.isStackOnLeft(mExpansionPoint); // 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, + mLayout.getChildCount(), + onLeft); + 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..3557906531b2 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,6 +24,7 @@ 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; @@ -42,6 +43,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; @@ -53,39 +56,53 @@ public class SplitLayoutTests extends ShellTestCase { @Mock SurfaceControl mRootLeash; @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), 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).onLayoutChanging(any(SplitLayout.class)); } @Test public void testSetDividePosition() { mSplitLayout.setDividePosition(anyInt()); - verify(mSplitLayoutHandler).onBoundsChanged(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutChanged(any(SplitLayout.class)); } @Test @@ -96,24 +113,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..c456c7de8821 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,6 +22,7 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.graphics.Rect; +import android.view.InsetsState; import android.view.SurfaceControl; import androidx.test.annotation.UiThreadTest; @@ -59,7 +60,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/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/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java index 56a005642ce2..69ead3ac9cf9 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 @@ -33,6 +33,7 @@ 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 +47,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; @@ -60,8 +61,8 @@ public class SideStageTests { 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); 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..736566e5b4d3 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 @@ -33,6 +33,7 @@ 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; @@ -65,9 +66,12 @@ 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) { super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage, - sideStage, imeController, splitLayout, transitions, transactionPool); + sideStage, imeController, insetsController, splitLayout, transitions, + transactionPool, logger); // 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..a53d2e8b1268 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 @@ -58,6 +58,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; @@ -79,9 +80,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; @@ -102,12 +105,14 @@ public class SplitTransitionTests extends ShellTestCase { mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); 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); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) .when(mTransitions).startTransition(anyInt(), any(), any()); @@ -131,6 +136,7 @@ public class SplitTransitionTests extends ShellTestCase { 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 +174,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 +195,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 +231,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 +253,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 +284,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()); @@ -298,6 +309,7 @@ public class SplitTransitionTests extends ShellTestCase { 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()); } @@ -335,10 +347,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..6cce0ab26df7 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 @@ -37,6 +37,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.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.transition.Transitions; @@ -57,8 +58,10 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private MainStage mMainStage; @Mock private SideStage mSideStage; @Mock private DisplayImeController mDisplayImeController; + @Mock private DisplayInsetsController mDisplayInsetsController; @Mock private Transitions mTransitions; @Mock private TransactionPool mTransactionPool; + @Mock private SplitscreenEventLogger mLogger; private StageCoordinator mStageCoordinator; @Before @@ -66,7 +69,8 @@ public class StageCoordinatorTests extends ShellTestCase { MockitoAnnotations.initMocks(this); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, - mDisplayImeController, null /* splitLayout */, mTransitions, mTransactionPool); + mDisplayImeController, mDisplayInsetsController, null /* splitLayout */, + mTransitions, mTransactionPool, mLogger); } @Test 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..1a30f164f9a8 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,13 @@ 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.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager; +import android.os.SystemProperties; import android.view.SurfaceControl; import android.view.SurfaceSession; @@ -52,6 +54,9 @@ 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; @@ -93,6 +98,8 @@ 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(); @@ -110,6 +117,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 d536adb9f8ae..160b3673aa8a 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 @@ -42,6 +42,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.testing.TestableContext; +import android.view.Display; import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; @@ -92,8 +93,8 @@ 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(); 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..54eacee8a9c3 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,6 +56,9 @@ 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; @@ -65,17 +76,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 +114,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 +133,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 +142,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 +216,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 +227,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(remoteFinishWCT); + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } @Override @@ -273,9 +289,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 +368,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 @@ -320,8 +403,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 +414,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(remoteFinishWCT); + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } @Override @@ -358,15 +440,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 +489,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 +525,73 @@ 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)); + } + 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 +609,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 +663,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 +727,46 @@ public class ShellTransitionTests { return taskInfo; } + private DisplayController createTestDisplayController() { + IWindowManager mockWM = mock(IWindowManager.class); + final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1]; + try { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) { + displayListener[0] = invocation.getArgument(0); + return null; + } + }).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(); + try { + displayListener[0].onDisplayAdded(DEFAULT_DISPLAY); + mMainExecutor.flushAll(); + } catch (RemoteException e) { + // Again, no remote stuff + } + 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/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/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; |